Dieser Artikel wurde maschinell übersetzt.

Concurrent Affairs

Vier Möglichkeiten zur Verwendung von Concurrency Runtime in Ihren C++-Projekten

Rick Molloy

Beispielcode herunterladen

Ich werde oft gefragt, wie Sie die neue Parallel computing Bibliotheken in Visual Studio 2010 Beta in vorhandene C++-Projekte integrieren. In diesem Artikel werde ich erläutern nur einige der Verwendungsmöglichkeiten für die APIs und Klassen, die Teil der Parallel Pattern Library (PPL), asynchrone Agents Library und Parallelität Runtime in Ihre vorhandenen Projekten sind. Ich werde durchlaufen vier allgemeine Szenarien Entwickler in Multithreadanwendung Entwicklung gegenüberstehen, und beschreiben, wie Sie produktiv sofort, sein können, mit dem PPL und Agents Bibliothek Ihre Multithreadprogramme effizienter und skalierbarer machen.

Eine: Verschieben von Arbeit von UI-Thread in ein Hintergrund-Task

Eines der ersten Dinge, die Sie angewiesen sind, als Windows-Entwickler zu vermeiden ist den Benutzeroberflächenthread hängende. Kontakt des Endbenutzers eines nicht reagierenden Fensters ist unglaublich unangenehmen unabhängig davon, ob Entwickler ihre Kunden mit einem Mauszeiger warten bereitstellen oder Windows bietet Ihnen eine nicht reagierende Benutzeroberfläche in Form von einem frosted Glas-Fenster. Die Anleitung, die wir angegeben haben, ist oft ziemlich kurz gefassten: keine blockierende Aufrufe auf dem Benutzeroberflächenthread ausführen, jedoch stattdessen diese Aufrufe an einen Hintergrundthread verschieben. Meiner Erfahrung ist, dass dieser Anleitung ausreichend nicht und, dass die Codierung Arbeit beteiligten mühsam, fehleranfällig und ziemlich unangenehme.

In diesem Szenario werde ich ein einfaches Beispiel serielles bereitstellen, das zum Arbeiten mit der vorhandenen threading-APIs verschieben. Anschließend werde ich zwei Ansätze zum Verschieben von Arbeit in einem Hintergrundthread mithilfe der PPL und Agents Bibliothek geben. Ich werde in diesem Szenario umbrochen, durch Blockieren der Beispiele zurück zu den Besonderheiten in einem UI-Thread.

Verschieben Sie eine serielle lang andauernde Operation in einem Hintergrund-Thread

So was bedeutet es, arbeiten in einem Hintergrundthread zu verschieben? Wenn eine Funktion, die eine lange ausgeführt oder potenziell blockierende Operation besteht und Sie möchten die Funktion in einem Hintergrundthread zu verschieben, ein gutes Angebot von Codebausteinen wird Code die Mechanismen der tatsächlich verschoben, dass die Arbeit beteiligt, auch für etwas so einfach wie eine einzelne Funktion aufrufen, wie die hier gezeigte:

void SomeFunction(int x, int y){
        LongRunningOperation(x, y);
}

Zunächst müssen Sie einen beliebigen Zustand zu verpacken, die verwendet werden, geht. Hier bin ich einfach Verpacken von ein Paar von ganzen Zahlen, so Verwendung konnte einen integrierten Container wie eine Std:: Vector, ein std::pair oder einer std::tuple, aber in der Regel was ich gesehen habe lediglich Leute führen Paket die Werte in Ihrer eigenen Struktur oder Klasse, wie folgt:

struct LongRunningOperationParams{
        LongRunningOperationParams(int x_, int y_):x(x_),y(y_){}
        int x;
        int y;
}

Müssen Sie erstellen eine globale oder statische Funktion, die der Threadpool oder CreateThread Signatur entspricht, unpackages, Zustand (i. d. r. durch Dereferenzieren eines Void * Zeiger) führt die Funktion, und löscht dann die Daten bei Bedarf. Beispiel:

DWORD WINAPI LongRunningOperationThreadFunc(void* data){
                LongRunningOperationParams* pData =
           (LongRunningOperationParams*) data;
                LongRunningOperation(pData->x,pData->y);
                //delete the data if appropriate
         delete pData;
}

Jetzt können Sie schließlich abrufen um tatsächlich Planung den Thread mit der Daten wie folgt aussieht:

void SomeFunction(int x, int y){
        //package up our thread state
        //and store it on the heap
        LongRunningOperationParams* threadData =
        new  LongRunningOperationParams(x,y);
        //now schedule the thread with the data
        CreateThread(NULL,NULL,&LongRunningOperationThreadFunc,
          (void*) pData,NULL);
}

Dies mag nicht wie viel Code. Technisch gesehen habe ich nur zwei Codezeilen SomeFunction, für unsere Klasse vier Zeilen und drei Zeilen für die Thread-Funktion hinzugefügt. Aber eigentlich vier Mal so viel Code. Wir wurden aus drei Zeilen von Code zu 12 Codezeilen genau so planen Sie einen einzigen Funktionsaufruf mit zwei Parametern. Das letzte Mal, das musste ich etwas sieht, glauben ich musste ungefähr acht Variablen zu erfassen, und erfassen und Festlegen dieser Zustand wird recht mühsam und fehleranfällig. Wenn ich korrekt erinnern, können Sie gefunden und behoben mindestens zwei Fehler nur im Prozess der Erfassen des Status und des Konstruktors erstellen.

Ich noch nicht auch betroffen, auf welche dauert der Thread abgeschlossen haben, warten, die in der Regel umfasst das Erstellen eines Ereignisses und ein Aufruf von WaitForSingleObject das Handle zu verfolgen und natürlich das Handle zu bereinigen, wenn Sie damit fertig sind. Mindestens drei weitere Codezeilen ist, und bleiben weiterhin, Behandlung von Ausnahmen und Rückgabecodes.

Alternative zu CreateThread: Task_group-Klasse

Der erste Ansatz werde beschreiben verwendet die Task_group-Klasse von der PPL. Wenn Sie nicht mit der Task_group-Klasse vertraut sind, stellt Methoden zum Starten von Aufgaben asynchron über task_group::run und Warten auf seine Aufgaben abgeschlossen über task_group::wait bereit. Außerdem bietet Abbruch der Aufgaben, die noch noch nicht gestartet wurde und enthält Funktionen für eine Ausnahme mit std::exception_ptr Verpacken und erneute Auslösen es.

Sehen Sie, dass erheblich weniger Code hier beteiligt ist als mit der CreateThread Ansatz, die vom Standpunkt der Lesbarkeit, der Code seriellen Beispiel wesentlich ähnlicher ist. Die erste Schritt besteht darin, ein Task_group-Objekt zu erstellen. Dieses Objekt muss gespeichert, an die Stelle, wo seine Lebensdauer verwaltet werden können – z. B. auf dem Heap oder als eine Member-Variable in einer Klasse. Als Nächstes verwenden Sie task_group::run zum Planen eines Tasks (nicht Thread), um die Arbeit zu erledigen. Task_group::Run einer Functor als Parameter akzeptiert und verwaltet die Lebensdauer der Functor für Sie. Mithilfe von C ++ 0 X Lambda, den Status zu verpacken Dies ist praktisch eine zweizeilige-Änderung an das Programm. Hier ist wie der Code aussieht:

//a task_group member variable
task_group backgroundTasks;
void SomeFunction(int x, int y){
        backgroundTasks.run([x,y](){LongRunningOperation(x, y);});
}

Arbeiten vornehmen mit der Bibliothek Agents asynchrone

Eine Alternative ist die Bibliothek Agents verwenden, die einen Ansatz auf Grundlage Nachrichtenübergabe umfasst. Der Umfang der Codeänderung ungefähr gleich ist, aber es gibt eine semantische Hauptunterschied durch einen Agent basierenden Ansatz hingewiesen. Anstatt Planen eines Tasks, Sie erstellen eine Pipeline Weiterleiten von Nachrichten und asynchron eine Nachricht zu senden, die nur die Daten enthält abhängig von der Pipeline selbst, um die Nachricht zu verarbeiten. Im vorherigen Fall möchte ich eine Nachricht mit x und y senden. Die Arbeit geschieht weiterhin auf einem anderen Thread aber nachfolgende Aufrufe an die gleiche Rohrleitung Warteschlange und die Nachrichten werden in Reihenfolge (im Gegensatz zu einer Task_group, die Reihenfolge garantiert nicht) verarbeitet.

Zunächst benötigen Sie eine Struktur, die Nachricht enthalten. Sie könnten dieselbe Struktur tatsächlich wie die früheren verwenden, aber ich werde es umbenennen, wie hier gezeigt:

struct LongRunningOperationMsg{
        LongRunningOperationMsg (int x, int y):m_x(x),m_y(y){}
        int m_x;
        int m_y;
}

Die nächste Schritt ist, einen Ort zum Senden der Nachricht zu deklarieren. In der Bibliothek Agents kann eine Nachricht an jede Nachricht Schnittstelle gesendet werden, die eine "Ziel"in diesem speziellen Fall am besten geeigneten ist jedoch Aufruf < T >. Ein Aufruf < T >akzeptiert eine Nachricht und wird mit einer Functor, die die Nachricht als Parameter annimmt. Die Deklaration und Konstruktion des Aufrufs könnte folgendermaßen aussehen (mit Lambdas):

call<LongRunningOperationMsg>* LongRunningOperationCall = new
   call<LongRunningOperationMsg>([]( LongRunningOperationMsg msg)
{
LongRunningOperation(msg.x, msg.y);
})

Die Änderung zu SomeFunction ist jetzt leichte. Das Ziel ist eine Nachricht erstellen und senden es asynchron an Aufrufobjekts. Der Aufruf wird auf einem separaten Thread aufgerufen werden, beim Empfang der Meldung:

void SomeFunction(int x, int y){
        asend(LongRunningOperationCall, LongRunningOperationMsg(x,y));
}

Erste Aufgaben zurück auf das UI-Thread

Abrufen von Arbeit aus dem UI-Thread ist nur die Hälfte des Problems. Vermutlich am Ende der LongRunningOperation Sie nun einige sinnvollen Ergebnis zu erzielen, und die nächste Schritt erhält häufig Arbeit wieder auf dem Benutzeroberflächenthread. Das Verfahren zum Übernehmen hängt von der Anwendung, aber die einfachste Methode dafür in den Bibliotheken in Visual Studio 2010 angeboten ist ein weiteres Paar von APIs und Nachricht blockiert aus der Bibliothek für Agents verwenden: Try_receive und Unbounded_buffer < T >.

Ein Unbounded_buffer < T >kann verwendet werden, zum Speichern einer Nachricht mit den Daten und potenziell den Code, der auf dem Benutzeroberflächenthread ausgeführt werden muss. Try_receive ist ein nicht blockierender API-Aufruf, der abzufragen, ob Daten zum Anzeigen verwendet werden können.

Wenn Sie auf dem Benutzeroberflächenthread Rendering Bilder, konnte Sie beispielsweise Code wie den folgenden verwenden, um Daten wieder auf dem Benutzeroberflächenthread erhalten, nachdem ein Aufruf an InvalidateRect:

unbounded_buffer<ImageClass>* ImageBuffer;
LONG APIENTRY MainWndProc(HWND hwnd, UINT uMsg,
  WPARAM wParam, LPARAM lParam)
{
    RECT rcClient;
    int i;
    ...
   ImageClass image;
   //check the buffer for images and if there is one there, display it.
   if (try_receive(ImageBuffer,image))
       DisplayImage(image);
   ...
}

Einige Details wie die Implementierung von der Meldungsschleife wurden hier weggelassen wurde, aber hoffe ich in diesem Abschnitt wurde hilfreich genug, um das Verfahren zu veranschaulichen. Fangen Sie an den Beispielcode für den Artikel, überprüfen Sie über eine vollständige funktionsfähiges Beispiel der einzelnen Ansätze.

Abbildung 1 ein nicht-threadsichere Klasse

samclass
Widget{
size_t m_width;
size_t m_height;
public:
Widget(size_t w, size_t h):m_width(w),m_height(h){};
size_t GetWidth(){
return m_width;
}
size_t GetHeight(){
return m_height;
}
void SetWidth(size_t width){
m_width = width;
}
void SetHeight(size_t height){
m_height = height;
}
};

  Verwalten von freigegebenen Status Message Blocks mit Agents

Eine weitere häufig auftretende Situation in Multithreadanwendung Entwicklung ist gemeinsam genutzten Zustand verwalten. Insbesondere, sobald Sie versuchen, die Kommunikation oder Freigeben von Daten zwischen Threads wird schnell Verwalten von gemeinsam genutzten Zustands zu einem Problem, dem müssen Sie den Umgang mit. Der Ansatz, den ich häufig gesehen habe, ist so einfach einen kritischen Abschnitt ein Objekt, seine Daten-Member und öffentliche Schnittstellen schützen hinzu, aber diese bald wird ein Problem Wartung und manchmal kann ein Leistungsproblem werden. In diesem Szenario werde ich ein Serien- und vereinfachte Beispiel mithilfe von Sperren durchlaufen, und ich werde dann zeigen, dass Alternative mit Message Blocks aus der Bibliothek für Agents.

Sperren einer einfachen Widget-Klasse

Abbildung 1 zeigt eine nicht threadsichere Widget-Klasse mit Breite und Höhe die Datenmember und einfache Methoden, die den Zustand zu ändern.

Der naive Ansatz für das vornehmen des Widget-Klasse Threads abgesicherten besteht darin, seine Methoden mit einem kritischen Abschnitt oder die Reader / Writer-Sperre zu schützen. Die PPL enthält eine Reader_writer_lock und Abbildung 2 bietet einen ersten Einblick in die offensichtliche Lösung der naive Ansatz: verwenden die Reader_writer_lock in die PPL.

Abbildung 2 mit Reader_writer_lock aus der Parallel Pattern Library

class LockedWidget{
size_t m_width;
size_t m_height;
reader_writer_lock lock;
public:
LockedWidget (size_t w, size_t h):m_width(w),m_height(h){};
size_t GetWidth(){
auto lockGuard = reader_writer::scoped_lock_read(lock);
return m_width;
}
size_t GetHeight(){
auto lockGuard = reader_writer::scoped_lock_read(lock);
return m_height;
}
void SetWidth(size_t width){
auto lockGuard = reader_writer::scoped_lock(lock);
m_width = width;
}
void SetHeight(size_t height){
auto lockGuard = reader_writer::scoped_lock(lock)
m_height = height;
}
};

Was ich hier getan haben ist ein Read_writer_lock als eine Membervariable hinzufügen und ergänzen alle entsprechende Methoden mit entweder dem Leser oder die Writer-Version der Sperre. Ich bin auch sicherzustellen, dass die Sperre ist nicht mit Scoped_lock-Objekten mitten in eine Ausnahme gehalten. Die Get-Methoden jetzt die Lesesperre erwerben und Set-Methoden die Schreibsperre erhalten. Technisch gesehen anscheinend dieser Ansatz ist richtig, aber das Design ist tatsächlich falsch und zerbrechliche insgesamt da seiner Schnittstellen kombiniert, nicht threadsicher sind. Insbesondere, wenn sich den folgenden Code bin ich wahrscheinlich fehlerhaft:

Thread1{
                SharedWidget.GetWidth();
                SharedWidget.GetHeight();
}
Thread2{
                SharedWidget.SetWidth();
                SharedWidget.SetHeight();
}

Da die Aufrufe von Thread1 und Thread2 verzahnt sein können, können Thread1 die Lesesperre für GetWidth erwerben und dann vor dem Aufruf von GetHeight SetWidth und SetHeight konnte beide ausführen. Damit, zusätzlich zum Schutz der Daten, Sie sicherstellen, dass die Schnittstellen an die Daten auch; korrekt sindDies ist eine der heimtückischsten Arten von Racebedingungen, da der Code korrekt sieht und die Fehler sehr schwierig sind, aufspüren. Naive Lösungen für diese Situation häufig angezeigt beinhalten eine Lock-Methode auf das Objekt selbst, oder schlimmer noch, eine Sperre gespeicherten woanders, die Entwickler daran denken, beim Zugriff auf das Widget erwerben zu müssen. Manchmal werden beide Ansätze verwendet.

Ein einfacher Ansatz wird sichergestellt, dass Schnittstellen sicher verzahnt werden können, ohne die Möglichkeit, den Zustand des Objekts zwischen überlappende Aufrufe einzureißen auszusetzen. Möglicherweise möchten Sie Ihre Schnittstelle weiterentwickelt, wie im Abbildung 3 Methoden GetDimensions und UpdateDimensions bereitzustellen. Diese Schnittstelle ist jetzt überraschend Verhalten führen, da die Methoden ermöglichen nicht unsichere Interleavings Verfügbarmachen weniger wahrscheinlich.

Abbildung 3 eine Version der Schnittstelle mit GetDimensions und UpdateDimensions Methoden

struct WidgetDimensions
{
size_t width;
size_t height;
};
class LockedWidgetEx{
WidgetDimensions m_dimensions;
reader_writer_lock lock;
public:
LockedWidgetEx(size_t w, size_t h):
m_dimensions.width(w),m_dimensions.height(h){};
WidgetDimensions GetDimensions(){
auto lockGuard = reader_writer::scoped_lock_read(lock);
return m_dimensions;
}
void UpdateDimensions(size_t width, size_t height){
auto lockGuard = reader_writer::scoped_lock(lock);
m_dimensions.width = width;
m_dimensions.height = height;
}
};

Verwalten von freigegebenen Status mit Message Blocks

Nachdem wir nehmen einen Blick auf wie der Agents Library helfen kann stellen verwalten einfacher Zustand und der Code etwas robuster freigegeben. Die wichtigsten Klassen aus der Bibliothek Agents, die eignen sich für Verwalten von freigegebene Variablen Overwrite_buffer < T >, der einen einzelnen aktualisierbaren Wert gespeichert und gibt eine Kopie der neuesten Wert beim empfangen wird, aufgerufen.Single_assignment < T >, welche Informationsspeicher und gibt eine Kopie einer einzelnen beim Wert erhalten wird aufgerufen, jedoch, wie eine Konstante zugewiesen werden kann nur einmal;und Unbounded_buffer < T >, der eine unbegrenzte Anzahl von Elementen (Arbeitsspeicher zulassen) und, wie eine FIFO-Warteschlange speichert dequeues das älteste Element Wenn wird aufgerufen.

Ich beginne mit einem Overwrite_buffer < T >. In der Widget-Klasse ich werde zunächst die M_dimensions-Membervariable mit Overwrite_buffer < WidgetDimensions > ersetzen, und dann ich werde die Methoden die expliziten Sperren entfernen und Ersetzen Sie durch die entsprechenden Sende- und Anrufe. Noch unsere Schnittstelle wird sicher kümmern muss, jedoch besteht nicht mehr müssen Sie die Daten zu sperren. Hier ist, wie dies im Code aussieht. Es ist tatsächlich etwas weniger Codezeilen als gesperrte Version und die gleiche Anzahl von Zeilen wie der seriellen Version:

class AgentsWidget{
    overwrite_buffer<WidgetDimensions> m_dimensionBuf;
public:
    AgentsWidget(size_t w, size_t h){
        send(&m_dimensionBuf,WidgetDimensions(w,h));
    };
    WidgetDimensions GetDimensions(){
        return receive(&m_dimensionBuf);
    }
    void UpdateDimensions(size_t width, size_t height){
        send(&m_dimensionBuf,WidgetDimensions(w,h));
    }
};

Es gibt ein feinen semantischer Unterschied hier aus der Reader_writer-Lock-Implementierung. Die Overwrite_buffer ermöglicht einen Aufruf von UpdateDimensions, die während eines Aufrufs von Dimensionen auftreten. Dadurch wird praktisch keine Blockierung während dieser Aufrufe, aber ein Aufruf von GetDimensions möglicherweise etwas veraltet. Es ist erwähnenswert, dass das Problem in der gesperrten Version auch vorhanden da, sobald Sie die Dimensionen erhalten, das Potenzial, veraltet sein. Ich hier getan haben lediglich den blockierenden Aufruf zu entfernen.

Ein Unbounded_buffer kann auch nützlich für die Widget-Klasse sein. Genommen Sie an, dass der feine semantische Unterschied beschriebenen unglaublich wichtig war. Wenn Sie eine Instanz eines Objekts, das sicherstellen sollen erfolgt durch nur einen Thread zu einem Zeitpunkt, können Unbounded_buffer als ein Objekt Inhaber, die Zugriff auf dieses Objekt verwaltet. Dies die Widget-Klasse zuweisen möchten, können M_dimensions entfernen und ersetzen es mit Unbounded_buffer < WidgetDimension >und verwenden Sie diesen Puffer über die Aufrufe von GetDimensions und UpdateDimensions. Die Herausforderung besteht hier darin um sicherzustellen, dass niemand können einen Wert aus unserer Widget während aktualisiert wird. Dies wird erreicht, indem die Unbounded_buffer geleert, sodass Aufrufe von GetDimension Warten der Aktualisierung gesperrt werden. Siehe Abbildung 4. GetDimensions und UpdateDimensions Block exklusiven Zugriff auf die Variable Dimensionen warten.

Abbildung 4 leeren Unbounded_Buffer,

class AgentsWidget2{
unbounded_buffer<WidgetDimensions> m_dimensionBuf;
public:
AgentsWidget2(size_t w, size_t h){
send(&m_dimensionBuf,WidgetDimensions(w,h));
};
WidgetDimensions GetDimensions(){
//get the current value
WidgetDimensions value = receive(&m_dimensionBuf);
//and put a copy of it right back in the unbounded_buffer
send(&m_dimensionBuf,value);
//return a copy of the value
return WidgetDimensions(value);
}
void UpdateDimensions (size_t width, size_t height){
WidgetDimensions oldValue = receive(&m_dimensionBuf);
send(&m_dimensionBuf,WidgetDimensions(width,height));
}
};

Es ist tatsächlich zu Coordinating Zugriff auf die Daten

Betonen Sie eine weitere Sache über unsere Widget-Klasse soll: sicherstellen, dass Methoden und Daten, die gleichzeitig zugegriffen werden können "sicher" funktionierengemeinsam ist kritisch. Dies kann häufig durch koordinierende Zugriff Zustand statt durch Sperren Methoden oder Objekten erreicht werden. Eine reine "Zeilen des Codes"im Hinblick auf die Sie kein großer Gewinn über gesperrte Beispiel angezeigt, und insbesondere das zweite Beispiel möglicherweise noch ein wenig mehr Code. Was erzielt wird, ist jedoch ein Design sicherer und mit ein wenig Gedanke häufig ändern serielle Schnittstellen so, dass der interne Zustand des Objekts ist nicht "unterbrochener." Im Beispiel Widget ich habe dies mithilfe der Nachricht blockiert, und ich konnte diesen Zustand in einer solchen Weise schützen, dass es sicherer ist. Hinzufügen von Methoden oder Funktionen für die Widget-Klasse ist in der Zukunft weniger wahrscheinlich um die interne Synchronisierung zerstören, die wir eingerichtet haben. Mit einer Sperre Member ist es ziemlich leicht, einfach vergessen, die Sperre zu sperren, wenn eine Methode für eine Klasse hinzugefügt wird. Aber verschieben Vorgänge zu einem Modell Weiterleiten von Nachrichten und Message Blocks mit, wie z. B. Daten und Klassen kann der überschreiben-Puffer auf Ihre natürliche Weise oft aufbewahren synchronisiert.

Drei: Verwenden kombinierbare für lokalen Thread-Accumulations und Initialisierung

Das zweite Szenario, in dem wir Zugriff auf ein Objekt mit Sperren oder Message Blocks geschützt funktioniert sehr gut für höhere Gewichtung Objekte, die selten zugegriffen werden. Falls beim Lesen von diesem Beispiel absehbar, dass es möglicherweise ein Leistungsproblem Wenn in einer Schleife enge (und parallele) synchronisierte Widget verwendet wurden, haben Sie vermutlich Recht. Das liegt daran Schützen von gemeinsam genutzten Zustand kann problematisch sein, und nicht für vollständig Allzweck-Algorithmen und Objekte, die tatsächlich Status Freigabe vorhanden Leider sind viele Optionen außer koordinieren Zugriff oder eine Sperre einführen. Aber finden Sie fast immer eine Möglichkeit, Code oder ein Algorithmus, die Abhängigkeit von gemeinsam genutzten Zustands zu lockern Umgestalten und einmal haben Sie hierzu einige bestimmte jedoch allgemeine Muster kombinierbare < T > ein Objekt ruftin der Parallel Pattern Library wirklich helfen aus.

Kombinierbare < T >ist ein gleichzeitiger Container, der Unterstützung für drei breiten Anwendungsfälle bietet: gedrückt halten, eine threadlokale Variable oder lokalen Thread-Initialisierung durchführen, Vorgänge assoziative Binärdatei (z. B. Summe, min und Mix) auf den lokalen Thread-Variablen und kombinieren und jeder lokalen Thread-Kopie mit einem Vorgang (wie Listen zusammen splicing) besuchen. In diesem Abschnitt werde ich erläutern jedem dieser Fälle und enthalten Beispiele für deren Verwendung.

Halten einer lokalen Thread-Variablen oder lokalen Thread-Initialisierung ausführen

Die erste Anwendungsfall erwähnt für kombinierbare < T >war für eine threadlokale Variable halten. Es ist relativ zum Speichern einer lokalen Thread-Kopie des globalen Zustands üblich. Z. B. in farbigem Ray Tracers Anwendungen wie in unserem Beispiel-Pack (code.msdn.microsoft.com/concrtextras) oder befindet sich die Beispiele für die parallele Entwicklung mit .NET 4.0 (code.msdn.microsoft.com/ParExtSamples) gibt es eine Option zum Kolorieren Sie jede Zeile von Thread auf die Parallelität zu visualisieren. In der systemeigenen Version der Demo erfolgt dies mithilfe von ein kombinierbarer Objekt, das die lokalen Thread-Farbe enthält.

Sie können eine threadlokale Variable natürlich halten, mithilfe von lokalen Threadspeicher (TLS), aber es gibt einige Nachteile – insbesondere Lebensdauerverwaltung Sichtbarkeit und diese gehen hand in hand. Um TLS verwenden, müssen Sie zunächst einen Index mit TlsAlloc reservieren, Ihr Objekt reservieren und speichern einen Zeiger auf das Objekt in den Index mit TlsSetValue. Wenn der Thread beendet wird, müssen Sie dann sicher, dass das Objekt freigegeben wird. (TlsFree wird automatisch aufgerufen.) Dies einmal oder zweimal pro Thread und sicherstellen, dass nicht vorhanden sind Verluste aufgrund von früh beendet oder Ausnahmen nicht, dass eine Herausforderung, aber wenn Ihre Anwendung Dutzende oder Hunderte von diese Elemente erforderlich ist, ein anderen Ansatz ist wahrscheinlich besser.

Kombinierbare < T >mit einen lokalen Thread-Wert verwendet werden können, aber die Lebensdauer der einzelnen Objekte sind an die Lebensdauer der kombinierbare < T > gebundenElement und ein großer Teil der Initialisierung ist automatisiert. Sie zugreifen den lokalen Thread-Wert, indem Sie einfach aufrufen, die combinable::local-Methode, die einen Verweis auf das lokale Objekt zurückgibt. Hier ist ein Beispiel zur Verwendung Task_group, aber dies kann mit Win32-Threads sowie durchgeführt werden:

combinable<int> values;

auto task = [&](){
                values.local() = GetCurrentThreadId();
                printf("hello from thread: %d\n",values.local());
};

task_group tasks;

tasks.run(task);

//run a copy of the task on the main thread
task();

tasks.wait();

Erwähnt, dass Thread-Local-Initialisierung auch mit kombinierbare erreicht werden kann. Wenn z. B. müssen Sie einen Bibliothek-Aufruf an jeden Thread initialisiert werden, auf dem es verwendet wird, können Sie eine Klasse erstellen, die die Initialisierung in seinem Konstruktor ausführt. Sie dann auf der ersten Verwendung pro Thread, der Bibliothek-Aufruf vorgenommen werden jedoch wird bei nachfolgenden Verwendungen übersprungen werden. Beispiel:

class ThreadInitializationClass
{
public:
      ThreadInitializationClass(){
            ThreadInitializationRoutine();
      };
};

...
//a combinable object will initialize these
combinable<ThreadInitializationClass> libraryInitializationToken;
            
...
//initialize the library if it hasn't been already on this thread
ThreadInitializationClass& threadInit = libraryInitalizationToken.local();

Durchführen von Reduzierung in einer parallelen Schleife

Ein weiteres wichtigsten Szenario für kombinierbare Objekt ist lokalen Thread-Reduzierung oder lokalen Thread-Accumulations durchführen. Insbesondere können Sie einen bestimmten Typ von Race-Bedingung beim Parallelisieren von Schleifen oder in rekursive parallele Traversalen mit kombinierbare vermeiden. Hier ist ein unglaublich vereinfachte Beispiel, die nicht zum Anzeigen von Speed-ups vorgesehen ist. Der folgende Code zeigt eine einfache Schleife, die aussieht, als es mit Parallel_for_each, außer für den Zugriff auf die Variable Summe parallelisiert werden kann:

 

int sum = 0;
for (vector<int>::iterator it = myVec.begin(); it != myVec.end(); ++it) {
    int element = *it;
    SomeFineGrainComputation(element);
    sum += element;
}

Anstatt eine Sperre in unserer Parallel_for_each, die Chance zerstört wir der Speed-ups mussten, platzieren können wir jetzt ein kombinierbarer-Objekt verwenden, um lokalen Thread-Summen berechnen:

combinable<int> localSums;
 
parallel_for_each(myVec.begin(),  myVec.end(), [&localSums] (int element) {
   SomeFineGrainComputation(element);
   localSums.local() += element;
});

Wir haben die Racebedingung jetzt erfolgreich vermieden jedoch wir haben eine Auflistung von lokalen Thread-Summen in dem LocalSums-Objekt gespeichert und wir müssen noch den letzten Wert extrahieren. Wir können dies mit der Methode kombinieren tun die eine binäre Functor wie folgt:

int sum = localSums.combine(std::plus<int>);

Der dritte Anwendungsfall für kombinierbare < T >, der mit der Combine_each-Methode, ist Wenn Sie müssen, besuchen Sie jede der lokalen Thread-Kopien, und führen eine Operation auf diese (wie bereinigen oder Fehlerüberprüfung). Ein anderes, interessanter wird z. B. das kombinierbare Objekt ist ein kombinierbarer < Liste < T > > und in Ihre Threads Sie std::lists oder std::sets erstellen. Im Fall von std::lists können Sie problemlos zusammen mit list::splice; spliced werdenmit std::sets können Sie mit set::insert eingefügt werden.

Vier: Konvertieren eines vorhandenen Hintergrund-Thread in ein Agent oder einer Aufgabe

Genommen Sie an, Sie bereits einen Hintergrund oder Worker Thread in Ihrer Anwendung haben. Es gibt einige sehr gute Gründe, warum möglicherweise möchten Sie die Hintergrund-Thread zu einem Vorgang aus die PPL oder in einen Agent zu konvertieren und dies daher relativ einfach. Einige der wichtigsten Vorteile dies gehören:

Komponierbarkeit und Leistung. Wenn den Arbeitsthreads werden intensiv berechnen und Sie erwägen werden, zusätzliche Threads in der PPL oder Agents Bibliothek verwenden, kann konvertieren den Hintergrund-Thread in einem Worker-Vorgang mit anderen Aufgaben in der Laufzeit zusammenarbeiten und vermeiden Oversubscription auf dem System.

Abbruch und Ausnahmebehandlung. Wenn Sie problemlos abbrechen Arbeit in einem Thread oder einen well-described Mechanismus zum Behandeln von Ausnahmen können möchten, hat eine Task_group diese integrierten Funktionen.

Steuern Sie Fluss und Status. Wenn müssen Sie den Status der Thread (gestartet oder abgeschlossen ist, für Beispiel) verwalten oder über ein Objekt, dessen Status effektiv vom Arbeitsthread inseparable ist, kann das Implementieren eines Agents nützlich sein.

Task_group bietet Abbruch und Ausnahmebehandlung

Im ersten Szenario untersucht wir, was es dauert, arbeiten mit einer Task_group planen: im Wesentlichen Verpacken Ihrer Arbeit in einer Functor (mithilfe von einen Lambda-Ausdruck, ein std::bind oder ein benutzerdefinierte Funktion-Objekt) und mit der task_group::run-Methode planen. Was beschrieben haben wurde der Abbruch und Ausnahmebehandlung Semantik, in der Tat verknüpft sind.

Abbildung 5 Implementierung von MyAgentClass

class MyAgentClass : public agent{
public:
MyAgentClass (){
}
AgentsWidget widget;
void run(){
//run is started asynchronously when agent::start is called
//...
//set status to complete
agent::done();
}
};

Zunächst werde ich einfach Semantik erläutern. Wenn Ihr Code aufgerufen, task_group::cancel wird oder eine Aufgabe eine nicht abgefangene Ausnahme auslöst, ist Absage für die Task_group in wirksam. Wenn Abbruch in Kraft ist, wird nicht Aufgaben, die auf die Task_group gestartet wurde noch nicht gestartet werden, wodurch Arbeit schnell und einfach auf eine Task_group abgebrochen werden. Abbruch unterbrechen nicht Aufgaben ausgeführt oder blockiert, damit eine laufende Aufgabe den Abbruch Status mit der task_group::is_canceling-Methode oder durch die Hilfsfunktion Abfragen können

Is_current_task_group_canceling. Hier ist ein kurze Beispiel:

task_group tasks;   
tasks.run([](){
    ...
    if(is_current_task_group_canceling())
    {
        //cleanup then return
        ...
        return;
    }
});
tasks.cancel();
tasks.wait();

Ausnahmebehandlung wirkt sich auf Abbruch, da eine nicht abgefangene Ausnahme in einem Task_group Abbruch auf die Task_group auslöst. Wenn eine nicht abgefangene Ausnahme vorhanden ist, wird die Task_group tatsächlich std::exception_ptr verwenden, um die Ausnahme im Thread Verpacken er ausgelöst wurde, auf. Später, wenn task_group::wait aufgerufen wird, wird die Ausnahme erneut auf dem Thread ausgelöst, die Wait aufgerufen.

Implementieren eines asynchronen Agents

Der Agents Library bietet eine Alternative zur Verwendung einer Task_group: Ersetzen einen Thread mit der Agent-Basisklasse. Wenn Ihre Thread viel threadspezifischen Zustand und Objekte verfügt, kann ein Agent eine bessere Übereinstimmung für das Szenario sein. Die Klasse abstract-Agent ist eine Implementierung das Actor-Musterdie beabsichtigte Verwendung ist das Implementieren einer eigene Klasse von Agent und dann alle Zustände, die Ihre Akteur (oder Thread) möglicherweise in diesen Agent kapseln. Felder, die öffentlich zugänglich sein sollen ist, die Anleitung werden als Nachricht blockiert oder Quellen und Ziele verfügbar machen und Nachrichtenübergabe, mit dem Agent kommunizieren.

Implementieren eines Agents erfordert eine Klasse von der Agent-Basisklasse ableiten und überschreiben die virtuelle Methode Ausführung. Der Agent kann durch Aufrufen von agent::start, der die run-Methode als Aufgabe, ähnlich wie ein Thread erstellt dann gestartet werden. Der Vorteil besteht darin, dass lokalen Thread-Zustand jetzt in der Klasse gespeichert werden kann. Auf diese Weise einfacher Synchronisierung des Status zwischen Threads, insbesondere, wenn der Status in Message Block gespeichert ist. Abbildung 5 zeigt ein Beispiel einer Implementierung, die eine Membervariable öffentlich verfügbar gemachte des Typs AgentsWidget verfügt.

Beachten Sie, die ich den Agentstatus festgelegt haben ausgeführt, da die run-Methode beendet wird. Dadurch wird den Agent nicht nur gestartet werden, sondern auch auf gewartet werden. Darüber hinaus kann aktuelle Status des Agents durch einen Aufruf von agent::status abgefragt werden. Starten und Warten auf unsere-Agent-Klasse ist einfach, wie der folgende Code zeigt:

 

MyAgentClass MyAgent;

//start the agent
MyAgent.start();

//do something else
...

//wait for the agent to finish
MyAgent.wait(&MyAgent);

Bonus-Element: Sortieren in Parallel mit parallel_sort

Schließlich möchte ich einen anderen potenziell leicht Punkt der Parallelisierung dieses Mal nicht aus der PPL oder der Agents Bibliothek jedoch aus unserem Beispiel Pack code.msdn.microsoft.com/concrtextras erhältlich vorschlagen. Parallele Quicksort ist eine der Beispiele verwenden wir für erläutert, wie rekursive und herrsche Algorithmen mit Aufgaben zu parallelisieren, und das Beispiel Pack enthält eine Implementierung der parallelen Quicksort. Parallele sortieren kann Speed-ups anzeigen, wenn Sie eine große Anzahl von Elementen sortieren, in dem der Vergleichsvorgang etwas teurer, als mit Zeichenfolgen ist. Es wird nicht wahrscheinlich Speed-ups für kleine Zahlen von Elementen oder anzeigen, wenn integrierte Typen wie ganze Zahlen und Double-Werte sortieren. Hier ist ein Beispiel wie es verwendet werden kann:

//from the sample pack
#include "parallel_algorithms.h"
int main()

using namespace concurrency_extras;
{
                vector<string> strings;
 
                //populate the strings
                ...
                parallel_sort(strings.begin(),strings.end());
}

Zusammenfassung

Ich hoffe, dieser Spalte hilft, erweitern Sie den Horizont des wie die parallelen Bibliotheken in Visual Studio 2010 auf Ihre Projekte über einfach mit Parallel_for oder Vorgänge beschleunigen berechnungsintensiv Schleifen angewendet werden. Viele Beispiele für andere hilfreich finden Sie in unserer Dokumentation auf MSDN (msdn.microsoft.com/library/dd504870(VS.100).aspx) und in unserem Beispiel-Pack (code.msdn.microsoft.com/concrtextras), die veranschaulichen, die parallelen-Bibliotheken und wie Sie verwendet werden können. Fangen Sie Sie auschecken.

Rick Molloy* ist Programmmanager im Parallel Computing Platform-Team bei Microsoft.*