Dieser Artikel wurde maschinell übersetzt.

Windows mit C++

Die Entwicklung der Synchronisierung in Windows und C++

Kenny Kerr

 

Kenny KerrAls ich anfing, gleichzeitige Schreibsoftware, hatte C++ keine Unterstützung für die Synchronisierung. Windows selbst hatte nur eine Handvoll Synchronisierungsprimitiven, die im Kernel implementiert wurden. Ich tendenziell einen kritischen Abschnitt verwenden, es sei denn, ich brauchte über Prozesse, synchronisiert, in diesem Fall habe ich einen Mutex. Allgemein beide sind Sperren oder Objekte zu sperren.

Der Mutex hat seinen Namen von dem Konzept der "wechselseitigen Ausschluss," ein anderer Name für die Synchronisation. Es bezieht sich auf die Garantie, dass nur ein Thread eine Ressource gleichzeitig zugreifen kann. Der kritische Abschnitt nimmt seinen Namen von der tatsächlichen Codeabschnitt, die solche Ressource zugreifen könnte. Um die Richtigkeit zu gewährleisten, kann nur ein Thread diese kritischen Abschnitt des Codes zu einem Zeitpunkt ausführen. Diese beiden Sperrobjekte verfügen über verschiedene Funktionen, aber es hat nur hilfreich zu wissen, dass sie sind gesperrt, beide Garantien wechselseitigen Ausschluss und beide können verwendet werden, um kritische Abschnitte des Codes abgrenzen.

Heute hat die Synchronisation-Landschaft dramatisch verändert. Es gibt eine Fülle von Möglichkeiten für den C++-Programmierer. Windows unterstützt nun viele weitere Synchronisierungsfunktionen und C++ selbst bietet endlich eine interessante Sammlung von Parallelität und Synchronisation-Funktionen für die Verwendung eines Compilers unterstützen die C ++ 11 Norm.

Im Artikel dieses Monats werde ich den Zustand der Synchronisierung in Windows und C++ zu erkunden. Ich werde beginnen mit einem Überblick über die von Windows selbst bereitgestellten Synchronisierungsprimitiven und dann erwägen, die alternativen bereitgestellt durch die C++-Standardbibliothek. Wenn Portabilität Ihr Hauptanliegen ist, werden die C++-Bibliothek-Neuzugänge sehr ansprechend sein. Wenn jedoch Portabilität weniger problematisch ist und Leistung paramount ist, dann immer mit was Windows vertraut jetzt werden Angebote wichtig. Wir tauchen direkt in.

Kritischer Abschnitt

Zunächst ist das kritische Abschnitt-Objekt. Diese Sperre wird stark von unzähligen Anwendungen verwendet, aber hat eine schmutzige Geschichte. Als ich begann, mit kritische Abschnitten, waren sie wirklich einfach. Um solch eine Sperre zu erstellen, alles, was Sie brauchte war eine CRITICAL_SECTION-Struktur und rufen die InitializeCriticalSection-Funktion, um es für die Verwendung vorbereitet. Diese Funktion schaltet nicht Wert, was bedeutet, dass es nicht scheitern kann. Damals jedoch war es notwendig für diese Funktion zum Erstellen von verschiedenen System-Ressourcen, insbesondere ein Kernel-Event-Objekt, und es war möglich, dass in extrem wenig Speicher Situationen dies fehlschlagen würde, wodurch eine strukturierte Ausnahme, die ausgelöst wird. Dennoch war dies eher selten, so dass die meisten Entwickler diese Möglichkeit ignoriert.

Mit der Popularität von COM explodiert die Verwendung von kritischen Abschnitten weil viele COM-Klassen, die kritische Abschnitte für die Synchronisation verwendet, aber in vielen Fällen gab es wenig bis keine tatsächliche Konflikte zu sprechen. Als Multiprozessorcomputern weiter verbreitet wurde, sah den kritischen Abschnitt Internes Ereignis noch weniger Verbrauch, weil der kritische Abschnitt kurz im Benutzermodus drehen würde, während des Wartens auf die Sperre. Eine kleine Spinninganzahl dazu geführt, dass viele kurzlebige Perioden des Anstoßes einen Kernel-Übergang, erheblich verbessert Leistung vermeiden könnten.

Um diese Zeit erkannte einige Kernel-Entwickler, dass sie die Skalierbarkeit von Windows erheblich verbessern könnten, wenn sie die Erstellung von kritischen Abschnitt Ereignisobjekte zurückgestellt, bis es genug Streit gab um ihre Anwesenheit erfordern. Dies schien wie eine gute Idee, bis die Entwickler, dass dies bedeutete erkannte, dass die EnterCriticalSection-Funktion (früher warten, bis Sperre Besitz) Obwohl InitializeCriticalSection jetzt nicht möglicherweise ausfallen könnte, nicht mehr zuverlässig war. Dies konnte nicht so leicht von Entwicklern, übersehen, weil es eine Vielzahl von Fehlerbedingungen eingeführt, die gemacht habe würde kritische Abschnitte unmöglich richtig verwenden und unzählige Anwendungen zu brechen. Dennoch konnte nicht die Skalierbarkeit gewinnt übersehen werden.

Ein Kernel-Entwickler kam endlich eine Lösung in Form einer neuen und undokumentierte, Kernel-Ereignisobjekts nannte eine freigestellte Event. Sie können ein wenig in dem Buch "Windows Internals," von Mark E. gelesen Russinovich, David A. Solomon und Alex Ionescu (Microsoft Press, 2012), im Grunde, anstatt ein Ereignisobjekt für jeden kritischen Abschnitt, ein einzelnes freigestellte Ereignis könnten jedoch für alle kritischen Abschnitten im System. Dies funktioniert, weil ein Ereignisobjekt für freigestellte genau das ist: Es stützt sich auf einen Schlüssel, der nur ein Zeigergröße Bezeichner ist, das ist natürlich Adressraum lokalen.

Es war sicherlich eine Versuchung, kritische Abschnitte um freigestellte Ereignisse verwenden ausschließlich zu aktualisieren, aber da viele Debugger und andere Werkzeuge auf die Interna von kritischen Abschnitten beruhen, das freigestellte Ereignis war nur verwendet als letztes Mittel, wenn der Kernel Fehler beim Zuordnen einer regelmäßigen Event-Objekt.

Das klingt wie viel irrelevant Geschichte aber für die Tatsache, dass die Leistung der Ereignisse eingegeben wurde während des Entwicklungszyklus von Windows Vista deutlich verbessert, und dies führte zu der Einführung einer völlig neuen Sperre Objekt sowohl einfacher und schneller war — aber mehr dazu in einer Minute.

Da das kritischen Abschnittsobjekt jetzt befreit von Ausfällen wegen zu wenig Speicher, ist es wirklich sehr einfach zu verwenden. Abbildung 1 stellt einen einfachen Wrapper.

Abbildung 1 die kritischen Abschnitt-Sperre

class lock
{
  CRITICAL_SECTION h;
  lock(lock const &);
  lock const & operator=(lock const &);
public:
  lock()
  {
    InitializeCriticalSection(&h);
  }
  ~lock()
  {
    DeleteCriticalSection(&h);
  }
  void enter()
  {
    EnterCriticalSection(&h);
  }
  bool try_enter()
  {
    return 0 != TryEnterCriticalSection(&h);
  }
  void exit()
  {
    LeaveCriticalSection(&h);
  }
  CRITICAL_SECTION * handle()
  {
    return &h;
  }
};

Die EnterCriticalSection-Funktion, die ich bereits erwähnt wird ergänzt durch eine TryEnterCriticalSection-Funktion, die eine nicht blockierende Alternative darstellt. Die LeaveCriticalSection-Funktion hebt die Sperre und DeleteCriticalSection frei alle Kernel-Ressourcen, die auf dem Weg zugeteilt worden sind, könnte.

So ist der kritische Abschnitt eine vernünftige Wahl. Er führt ganz sowie Kernel Übergänge und Zuweisung von Ressourcen zu vermeiden versucht. Trotzdem hat es einiges an Gepäck, die es aufgrund seiner Geschichte und Anwendungskompatibilität vornehmen muss.

Mutex

Das Mutex-Objekt ist eine echte Kernel-Synchronisierung-Objekt. Im Gegensatz zu kritische Abschnitte verbraucht eine Mutex-Sperre immer Kernel zugewiesenen Ressourcen. Der Vorteil ist natürlich, dass der Kernel dann prozessübergreifende Synchronisierung aufgrund seiner Bewusstsein der Sperre bereitstellen kann. Als Kernelobjekt, es bietet die üblichen Attribute — z.B. ein —, verwendet werden kann, um das Objekt von anderen Prozessen zu öffnen oder um die Sperre in einem Debugger zu identifizieren. Sie können auch eine Zugriffsmaske zum Einschränken des Zugriffs auf das Objekt angeben. Als eine intraprocess Sperre ist es übertrieben, ein wenig mehr kompliziert zu bedienen und viel langsamer. Abbildung 2 stellt einen einfachen Wrapper für eine unbenannte Mutex, die effektiv Prozess-lokal ist.

Abbildung 2 die Mutex-Sperre

#ifdef _DEBUG
  #include <crtdbg.h>
  #define ASSERT(expression) _ASSERTE(expression)
  #define VERIFY(expression) ASSERT(expression)
  #define VERIFY_(expected, expression) ASSERT(expected == expression)
#else
  #define ASSERT(expression) ((void)0)
  #define VERIFY(expression) (expression)
  #define VERIFY_(expected, expression) (expression)
#endif
class lock
{
  HANDLE h;
  lock(lock const &);
  lock const & operator=(lock const &);
public:
  lock() :
    h(CreateMutex(nullptr, false, nullptr))
  {
    ASSERT(h);
  }
  ~lock()
  {
    VERIFY(CloseHandle(h));
  }
  void enter()
  {
    VERIFY_(WAIT_OBJECT_0, WaitForSingleObject(h, INFINITE));
  }
  bool try_enter()
  {
    return WAIT_OBJECT_0 == WaitForSingleObject(h, 0);
   }
  void exit()
  {
    VERIFY(ReleaseMutex(h));
  }
  HANDLE handle()
  {
    return h;
  }
};

Die CreateMutex-Funktion erstellt die Sperre und die gemeinsame CloseHandle-Funktion schließt den Prozess zu behandeln, die effektiv verringert die Sperre Verweiszähler im Kernel. Warten auf Sperre Besitz wird erreicht mit der universell einsetzbare WaitForSingleObject-Funktion überprüft und optional wartet auf den signalisierten Zustand einer Vielzahl von Kernel-Objekte; Der zweite Parameter gibt an, wie lange der aufrufende Thread blockiert werden soll, während des Wartens auf die Sperre. Die unendliche Konstante ist – nicht überraschend – eine unbegrenzte Wartezeit, während der Wert NULL verhindert, dass den Thread wartet auf allen und die Sperre nur erwerben wird, wenn es frei ist. Schließlich hebt die ReleaseMutex-Funktion die Sperre.

Die Mutex-Sperre ist einen großen Hammer mit viel Power, aber es kommt zu einem Preis zu Leistung und Komplexität. Diese Verpackung Abbildung 2 ist übersät mit Behauptungen an, die Möglichkeiten, dass es ausfallen kann, aber es ist die Auswirkungen auf die Leistung, die die Mutex-Sperre in den meisten Fällen zu disqualifizieren.

Veranstaltung

Bevor ich über eine Hochleistungs-Sperre zu sprechen, muss ein weiteres Kernel-Synchronisations-Objekt, eine Einführung, denen ich bereits angedeutet. Obwohl nicht wirklich eine Sperre, insofern es nicht, eine Einrichtung zur wechselseitigen Ausschluss direkt zu implementieren bietet, das Ereignisobjekt ist lebenswichtig für die Koordinierung der Arbeit zwischen Threads. In der Tat ist es das gleiche Objekt wird intern von den kritischen Abschnitt Sperren — und außerdem es ist praktisch, wenn alle Arten von Parallelität Mustern auf effiziente und skalierbare Weise implementieren.

Die CreateEvent-Funktion erstellt das Ereignis und — wie den Mutex — die CloseHandle-Funktion schließt das Handle, die Freigabe des Objekts im Kernel. Weil es eigentlich eine Sperre nicht, hat es keine erwerben/Release-Semantik. Es ist vielmehr die genaue Verkörperung der Signalisierung Funktionalität bereitgestellt durch viele Kernelobjekte. Um zu verstehen wie die Signalisierung funktioniert, müssen Sie schätzen, dass ein Event-Objekt in einem der beiden Staaten erstellt werden kann. Wenn Sie true CreateEvent des zweiten Parameter übergeben, wird das resultierende Ereignisobjekt sagte, ein manuelles Zurücksetzen Event; Andernfalls wird ein automatisches Zurücksetzen-Ereignis erstellt. Ein manuelles Zurücksetzen-Ereignis erfordert, dass Sie manuell festlegen und den Zustand des Objekts signalisiert zurückgesetzt. Die Funktionen SetEvent und ResetEvent sind für diesen Zweck bereitgestellt. Ein Ereignis für automatisches Zurücksetzen automatisch­matisch zurückgesetzt (Änderungen aus signalisiert zu nicht signalisiert) Wenn ein wartender Thread freigegeben ist. Daher ist ein Ereignis für automatisches Zurücksetzen sinnvoll, wenn ein Thread muss mit einem anderen Thread zu koordinieren, während ein manuelles Zurücksetzen-Ereignis nützlich, ist wenn ein Thread mit einer beliebigen Anzahl von Threads zu koordinieren muss. Aufrufen von SetEvent für ein automatisches Zurücksetzen-Ereignis wird höchstens ein Thread freigeben, während mit ein manuelles Zurücksetzen-Ereignis so Will nennen alle wartenden Threads freizugeben. Mit der WaitForSingleObject-Funktion wird wie der Mutex wartet auf ein Ereignis signalisiert wird erzielt. Abbildung 3 stellt einen einfachen Wrapper für ein unbenannter Ereignis, das in beiden Modi konstruiert werden kann.

Abbildung 3 das Event Signal

class event
{
  HANDLE h;
  event(event const &);
  event const & operator=(event const &);
public:
  explicit event(bool manual = false) :
    h(CreateEvent(nullptr, manual, false, nullptr))
  {
    ASSERT(h);
  }
  ~event()
  {
    VERIFY(CloseHandle(h));
  }
  void set()
  {
    VERIFY(SetEvent(h));
  }
  void clear()
  {
    VERIFY(ResetEvent(h));
  }
  void wait()
  {
    VERIFY_(WAIT_OBJECT_0, WaitForSingleObject(h, INFINITE));
  }
};

SRW-Sperre (Slim Reader/Writer)

Der Name der Slim Reader/Writer (SRW) Sperre möglicherweise einen Schluck, aber das wichtige Wort ist "schlank". Programmierer möglicherweise übersehen diese Sperre wegen seiner Fähigkeit zu unterscheiden zwischen freigegebenen Leser und exklusiven Autoren, vielleicht denken, dass dies übertrieben ist, wenn alles, was sie brauchen ist ein kritischer Abschnitt. Es stellt sich heraus, das ist die einfachste Schleuse zu bewältigen und auch bei weitem die schnellste, und Sie brauchen sicherlich nicht Leser geteilt haben, um es zu verwenden. Es hat diese schnelle Ruf, nicht nur, weil es für das effiziente freigestellte Ereignisobjekt sondern auch, beruht weil es meistens im Benutzermodus implementiert wird und nur zurück in den Kernel, fällt wenn Konflikte ist, dass der Thread besser schlafen wäre. Auch die kritischen Abschnitt und Mutex-Objekte bieten zusätzliche Features, die Sie, die z. b. rekursive oder prozessübergreifenden Sperren benötigen könnten, aber mehr als oft nicht, alles was Sie brauchen ist eine schnelle und leichtgewichtige Sperre für den internen Gebrauch.

Diese Sperre stützt sich ausschließlich auf die freigestellte Ereignisse, die, denen ich bereits erwähnt habe, und als solches ist es extrem leicht, trotz der Bereitstellung von viel Funktionalität. Die SRW-Sperre erfordert nur einen Zeiger -­große Menge an Speicher, der von der aufrufende Prozess anstatt der Kernel zugeordnet wird. Aus diesem Grund die Initialisierungsfunktion, InitializeSRWLock, kann nicht umhin und lediglich gewährleistet, dass die Sperre das entsprechende Bitmuster enthält, bevor Sie verwendet werden.

Wartet auf Sperre Besitz erreicht wird, verwenden entweder die Acquire­SRWLockExclusive-Funktion für eine so genannte Schreibsperre oder mithilfe der AcquireSRWLockShared-Funktion für Leser sperren. Die exklusive und freigegebene Terminologie ist jedoch besser geeignet. Es gibt entsprechende Freigabe und versuchen-Funktionen wie erwerben eines erwarten, für beide exklusiv und Modi geteilt. Abbildung 4 stellt einen einfachen Wrapper für eine exklusive Sperre SRW. Es wäre nicht schwer für die Laufzeit-Modus-Funktionen hinzufügen, wenn nötig. Beachten Sie jedoch, dass dort kein Destruktor ist, denn es keine Ressourcen gibt freigeben.

Abbildung 4 die SRW-Sperre

class lock
{
  SRWLOCK h;
  lock(lock const &);
  lock const & operator=(lock const &);
public:
  lock()
  {
    InitializeSRWLock(&h);
  }
  void enter()
  {
    AcquireSRWLockExclusive(&h);
  }
  bool try_enter()
  {
    return 0 != TryAcquireSRWLockExclusive(&h);
  }
  void exit()
  {
    ReleaseSRWLockExclusive(&h);
  }
  SRWLOCK * handle()
  {
    return &h;
  }
};

Bedingungsvariable

Das endgültige Synchronisierungsobjekt brauche ich zur Einführung ist die Bedingungsvariable. Dies ist vielleicht diejenige, mit der meisten Programmierer nicht vertraut werden. Ich habe, jedoch ein erneutes Interesse an Bedingungsvariablen in den letzten Monaten bemerkt. Dies hätte etwas mit C ++ 11, aber die Idee ist nicht neu und die Unterstützung für dieses Konzept einige Zeit unter Windows schon seit. In der Tat hat das Microsoft .NET Framework das Bedingung Variable Muster seit der ersten Veröffentlichung unterstützt obwohl sie mit der Monitor-Klasse zusammengeführt wurde seine Nützlichkeit in gewisser Weise zu begrenzen. Aber dieses erneuerte Interesse ist auch dank der erstaunlichen freigestellte Ereignisse, die Bedingungsvariable durch Windows Vista eingeführt werden dürfen, und sie haben nur seit verbessert. Obwohl eine Bedingungsvariable lediglich ein Muster von Parallelität ist und somit mit anderen primitiven umgesetzt werden kann, also seine Aufnahme in die OS es erreichen erstaunliche Leistung und befreit den Programmierer müssen die Richtigkeit solcher Code zu gewährleisten. Wenn Sie OS Synchronisierungsprimitiven beschäftigt sind, ist es fast unmöglich, die Richtigkeit der einige Parallelität-Muster ohne die Hilfe des Betriebssystems selbst zu gewährleisten.

Das Bedingung Variable Muster ist durchaus üblich, wenn man darüber nachdenkt. Ein Programm muss warten, bis eine Bedingung erfüllt sein, bevor sie fortfahren kann. Auswerten dieser Bedingung beinhaltet, wozu Sie eine Sperre um einige Freigabezustand zu evaluieren. Wenn jedoch die Bedingung noch erfüllt hat noch keine, muss die Sperre freigegeben werden, damit um einige andere Threads, die Bedingung zu erfüllen zu können. Der bewertende Thread muss dann warten, so lange, bis die Bedingung erfüllt ist, bevor die Sperre erneut zu erwerben. Sobald die Sperre erneut erhalten, ist, muss die Bedingung erneut ausgewertet werden, um die offensichtliche Racebedingung zu vermeiden. Umsetzung ist schwieriger als es scheint, denn es in der Tat gibt andere Fallstricke zu kümmern – und sie effizient zu implementieren ist noch härter. Der folgende Pseudocode zeigt das Problem:

lock-enter
while (!condition-eval)
{
  lock-exit
  condition-wait
  lock-enter
}
// Do interesting stuff here
lock-exit

Aber auch in dieser Abbildung liegt einen subtilen Fehler. Um ordnungsgemäß zu funktionieren, muss die Bedingung auf gewartet werden, bevor die Sperre beendet, aber dadurch würde nicht so funktionieren, da die Sperre dann nie freigegeben würden. Die Fähigkeit, atomar ein Objekt freigeben und auf ein anderes warten ist so wichtig, dass Windows die SignalObjectAndWait-Funktion, um Kernelobjekte also für bestimmte tun bietet. Aber da die SRW-Sperre meist im Benutzermodus lebt, eine andere Lösung ist erforderlich. Bedingungsvariablen eingeben.

Wie die SRW-Sperre die Bedingungsvariable belegt nur eine einzige Zeigergröße Menge an Speicher und wird mit der ausfallsicheren InitializeConditionVariable-Funktion initialisiert. Wie bei SRW-Sperren, gibt es keine Ressourcen frei, also wenn die Bedingungsvariable nicht mehr erforderlich ist der Speicher einfach zurückgefordert werden kann.

Da die Bedingung selbst programmspezifische ist, es bleibt der Anrufer das Muster als eine Weile schreiben Schleife mit dem Körper wird von einem einzigen Aufruf der Funktion "SleepConditionVariableSRW". Diese Funktion hebt die SRW-Sperre atomar Erwartung geweckt werden, sobald die Bedingung erfüllt ist. Es gibt auch eine entsprechende SleepConditionVariableCS-Funktion, wenn Sie stattdessen Bedingungsvariable mit einem kritischen Abschnitt verwenden möchten.

Die WakeConditionVariable-Funktion wird aufgerufen, um einen einzigen wartenden wake oder schlafen, Gewinde. Der geweckte Thread wird die Sperre kehrte erneut zu erhalten. Alternativ kann die WakeAllConditionVariable-Funktion verwendet werden, alle wartenden Threads zu wecken. Abbildung 5stellt einen einfachen Wrapper mit den erforderlichen while-Schleife. Beachten Sie, dass es möglich für den schlafenden Thread unvorhersehbar geweckt werden, und die While-Schleife wird sichergestellt, dass die Bedingung immer erneut geprüft wird, nachdem der Thread die Sperre erneut erhält. Es ist auch wichtig zu beachten, dass das Prädikat immer ausgewertet wird, während die Sperre.

Abbildung 5 die Bedingungsvariable

class condition_variable
{
  CONDITION_VARIABLE h;
  condition_variable(condition_variable const &);
  condition_variable const & operator=(condition_variable const &);
public:
  condition_variable()
  {
    InitializeConditionVariable(&h);
  }
  template <typename T>
  void wait_while(lock & x, T predicate)
  {
    while (predicate())
    {
      VERIFY(SleepConditionVariableSRW(&h, x.handle(), INFINITE, 0));
    }
  }
  void wake_one()
  {
    WakeConditionVariable(&h);
  }
  void wake_all()
  {
    WakeAllConditionVariable(&h);
  }
};

Warteschlange blockiert

Um einige dieser Form geben, verwende ich eine blockierende Warteschlange als Beispiel. Ich möchte betonen, dass ich empfehlen nicht blockieren Warteschlangen im Allgemeinen. Sie können besser bedient mit einer Abschluss-i/o-Port oder Windows-Thread-Pool, die nur eine Abstraktion über das ehemalige oder sogar die Parallelität Runtime Concurrent_queue-Klasse ist. Es ist alles nicht blockierenden generell bevorzugt. Trotzdem ist eine blockierende Warteschlange ein einfaches Konzept zu begreifen, und etwas, was viele Entwickler scheinen nützlich finden. Zugegeben, nicht jedes Programm muss skalieren, aber jedes Programm muss korrekt sein. Eine blockierende Warteschlange bietet auch reichlich Gelegenheit, Synchronisation auf Richtigkeit zu beschäftigen – und natürlich reichlich Gelegenheit es falsch ist.

Sollten Sie implementieren eine blockierende Warteschlange mit nur eine Sperre und ein Ereignis. Die Sperre schützt die freigegebene Warteschlange und das Ereignis signalisiert ein Verbraucher, dass ein Hersteller etwas in die Ereigniswarteschlange geschoben hat. Abbildung 6 bietet ein einfaches Beispiel verwendet ein automatisches Zurücksetzen-Ereignis. Ich habe dieses Ereignismodus, weil die Push-Methode in die Warteschlange nur eines einzelnen Elements und so, ich nur ein Verbraucher geweckt werden, um es aus der Warteschlange pop will. Die Push-Methode erhält die Sperre, das Element in die Warteschlange und dann das Ereignis aufzuwachen jeden wartende Verbraucher signalisiert. Die pop-Methode erhält die Sperre und dann wartet, bis die Warteschlange nicht vor dem Entfernen eines Elements und er es leer ist. Beide Methoden verwenden eine Lock_block-Klasse. Aus Platzgründen wurde es nicht im Preis inbegriffen, aber es ruft einfach der Sperre enter-Methode in seinem Konstruktor und die Exit-Methode in der Destruktor.

Abbildung 6 automatisches Zurücksetzen blockieren Warteschlange

template <typename T>
class blocking_queue
{
  std::deque<T> q;
  lock x;
  event e;
  blocking_queue(blocking_queue const &);
  blocking_queue const & operator=(blocking_queue const &);
public:
  blocking_queue()
  {
  }
  void push(T const & value)
  {
    lock_block block(x);
    q.push_back(value);
    e.set();
  }
  T pop()
  {
    lock_block block(x);
    while (q.empty())
    {
      x.exit(); e.wait(); // Bug!
      x.enter();
    }
    T v = q.front();
    q.pop_front();
    return v;
  }
};

Beachten Sie jedoch, der wahrscheinlich Sackgasse, weil die Ausfahrt und warten-Aufrufe nicht atomare. Wenn die Sperre einen Mutex waren, ich konnte die SignalObjectAndWait-Funktion verwenden, aber die Leistung der blockierende Warteschlange würde leiden.

Eine weitere Möglichkeit ist ein manuelles Zurücksetzen-Ereignis verwenden. Anstatt signalisiert, wenn ein Element in der Warteschlange befindet, definieren Sie einfach zwei Staaten. Das Ereignis kann für signalisiert werden, solange gibt es Elemente in der Warteschlange und nicht signalisiert, wenn es leer ist. Dies wird auch viel bessere Leistung, da es weniger Aufrufe in den Kernel gibt, um das Ereignis zu signalisieren. Abbildung 7 liefert ein Beispiel dafür. Beachten Sie, wie die Push-Methode das Ereignis legt, wenn die Warteschlange ein Element hat. Dies vermeidet unnötige Aufrufe der Funktion SetEvent. Die pop-Methode löscht pflichtgemäß das Ereignis, wenn die Warteschlange leer gefunden. Solange mehrere in der Warteschlange Elemente vorhanden sind, kann eine beliebige Anzahl von Verbrauchern Elemente aus der Warteschlange pop, ohne Einbeziehung des Event-Objekts, so die Skalierbarkeit zu verbessern.

Abbildung 7 manuelles Zurücksetzen blockieren Warteschlange

template <typename T>
class blocking_queue
{
  std::deque<T> q;
  lock x;
  event e;
  blocking_queue(blocking_queue const &);
  blocking_queue const & operator=(blocking_queue const &);
public:
  blocking_queue() :
    e(true) // manual
  {
  }
  void push(T const & value)
  {
    lock_block block(x);
    q.push_back(value);
    if (1 == q.size())
    {
      e.set();
    }
  }
  T pop()
  {
    lock_block block(x);
    while (q.empty())
    {
      x.exit();
      e.wait();
      x.enter();
    }
    T v = q.front();
    q.pop_front();
    if (q.empty())
    {
      e.clear();
    }
    return v;
  }
};

In diesem Fall besteht keine potenzielle Deadlock in der Ausfahrt-warten-EINGABETASTE-Sequenz da einen anderen Verbraucher das Ereignis stehlen kann nicht angesichts der Tatsache, dass es ein manuelles Zurücksetzen-Ereignis ist. Es ist schwer zu schlagen in Bezug auf Leistung. Dennoch eine Alternative (und vielleicht mehr natürlichen) Lösung ist, eine Bedingungsvariable anstelle eines Ereignisses zu verwenden. Dies geschieht ganz einfach mit der Condition_variable-Klasse in Abbildung 5 und ähnelt der manuelles Zurücksetzen blockierende Warteschlange, obwohl es ein wenig einfacher ist. Abbildung 8 zeigt anhand eines Beispiels. Beachten Sie, wie die Semantik und Parallelität Absichten deutlicher als die übergeordneten Synchronisierungsobjekte beschäftigt sind. Diese Klarheit hilft um Parallelitätsfehler zu vermeiden, die oft mehr-dunkel-Code zu quälen.

Abbildung 8 Condition Variable Blockierung Warteschlange

template <typename T>
class blocking_queue
{
  std::deque<T> q;
  lock x;
  condition_variable cv;
  blocking_queue(blocking_queue const &);
  blocking_queue const & operator=(blocking_queue const &);
public:
  blocking_queue()
  {
  }
  void push(T const & value)
  {
    lock_block block(x);
    q.push_back(value);
    cv.wake_one();
  }
  T pop()
  {
    lock_block block(x);
    cv.wait_while(x, [&]()
    {
      return q.empty();
    });
    T v = q.front();
    q.pop_front();
    return v;
  }
};

Schließlich sollte ich erwähnen, dass C ++ 11 stellt jetzt eine Sperre, genannt ein Mutex sowie ein Condition_variable. Die C ++ 11 Mutex hat nichts mit der Windows-Mutex. Ebenso die C ++ 11 Condition_variable ist nicht auf die Windows-Bedingungsvariable basiert. Dies ist eine gute Nachricht in Bezug auf Portabilität. Es kann verwendet werden überall finden Sie einen konformen C++-Compiler. Auf der anderen Seite, die C ++ 11 Implementierung in der Visual C++-2012-Version führt ganz schlecht im Vergleich zu Windows SRW-Sperre und Zustand-Variable. Abbildung 9 enthält ein Beispiel für eine blockierende Warteschlange implementiert mit der Standard C ++ 11 Bibliothekstypen.

Abbildung 9 ++ 11 blockieren Warteschlange

template <typename T>
class blocking_queue
{
  std::deque<T> q;
  std::mutex x;
  std::condition_variable cv;
  blocking_queue(blocking_queue const &);
  blocking_queue const & operator=(blocking_queue const &);
public:
  blocking_queue()
  {
  }
  void push(T const & value)
  {
    std::lock_guard<std::mutex> lock(x);
    q.push_back(value);
    cv.notify_one();
  }
  T pop()
  {
    std::unique_lock<std::mutex> lock(x);
    cv.wait(lock, [&]()
    {
      return !q.empty();
    });
    T v = q.front();
    q.pop_front();
    return v;
  }
};

Die Implementierung der C++-Standardbibliothek wird zweifellos mit der Zeit verbessern, wie die Bibliothek Unterstützung für Parallelität im Allgemeinen wird. Die C++-Komitee hat einige kleine, konservative Schritte in Richtung Gleichzeitigkeitsunterstützung, die erkannt werden sollen, aber die Arbeit ist noch nicht gemacht. Wie ich in meinem letzten drei Spalten erläutert, ist die Zukunft von C++ Parallelität immer noch in Frage. Es ist die Kombination von einigen hervorragenden Synchronisierungsprimitiven in Windows und der Zustand-von-der-Art-C++-Compiler für eine überzeugende Toolkit für die Herstellung von leichter und skalierbarer Parallelität-Tresor-Programs.

Kenny Kerr ist Softwarespezialist mit dem Schwerpunkt auf der systemeigenen Windows-Entwicklung. Sie erreichen ihn unter kennykerr.ca.

Unser Dank gilt dem folgenden technischen Experten für die Durchsicht dieses Artikels: Mohamed Ameen Ibrahim