Aufgabenparallelität (Concurrency Runtime)
In diesem Dokument wird die Rolle von Aufgaben und Aufgabengruppen in der Concurrency Runtime beschrieben. Es wird empfohlen, Arbeitsgruppen zu verwenden, wenn Sie zwei oder mehr unabhängige Arbeitsaufgaben gleichzeitig ausführen möchten. Nehmen Sie zum Beispiel einmal an, dass Sie über einen rekursiven Algorithmus verfügen, der die verbleibende Arbeit in zwei Partitionen unterteilt. Mithilfe von Arbeitsgruppen können Sie diese Partitionen gleichzeitig ausführen. Wenn Sie die gleiche Routine parallel auf jedes Element einer Auflistung anwenden möchten, wird hingegen empfohlen, parallele Algorithmen wie Concurrency::parallel_for zu verwenden. Weitere Informationen zu parallelen Algorithmen finden Sie unter Parallele Algorithmen.
Aufgaben und Aufgabengruppen
Eine Aufgabe ist eine Arbeitseinheit, die einen bestimmten Auftrag ausführt. Eine Aufgabe kann i. d. parallel zu anderen Aufgaben ausgeführt und in detailliertere Einzelaufgaben zerlegt werden. Eine Reihe von Aufgaben wird in einer Aufgabengruppe organisiert. Aufgabengruppen verschieben Aufgaben in eine Arbeitsübernahme-Warteschlange. Der Planer entfernt Aufgaben aus dieser Warteschlange und führt sie auf verfügbaren Computerressourcen aus. Nachdem Sie einer Aufgabengruppe Aufgaben hinzugefügt haben, können Sie warten, bis alle Aufgaben aufgeführt wurden, oder Sie können Aufgaben abbrechen, die noch nicht gestartet wurden.
Aufgabengruppen werden von der PPL mit der Concurrency::task_group-Klasse und der Concurrency::structured_task_group-Klasse dargestellt, und Aufgaben werden mit der Concurrency::task_handle-Klasse dargestellt. In der task_handle-Klasse wird der Code gekapselt, der die Arbeit ausführt. Dieser Code liegt in Form einer Lambda-Funktion, eines Funktionszeigers oder eines Funktionsobjekts vor und wird häufig als Arbeitsfunktion bezeichnet. In der Regel ist es nicht erforderlich, direkt mit task_handle-Objekten zu arbeiten. Stattdessen übergeben Sie Arbeitsfunktionen an eine Aufgabengruppe, die die task_handle-Objekte erstellt und verwaltet.
Die PPL unterscheidet zwischen zwei Kategorien von Aufgabengruppen: unstrukturierte Aufgabengruppen und strukturierte Aufgabengruppen. Die PPL stellt unstrukturierte Aufgabengruppen mit der task_group-Klasse und strukturierte Aufgabengruppen mit der structured_task_group-Klasse dar.
Wichtig
Außerdem definiert die PPL den Concurrency::parallel_invoke-Algorithmus, der eine Reihe von Aufgaben mit der structured_task_group-Klasse parallel ausführt. Da der parallel_invoke-Algorithmus eine kompaktere Syntax aufweist, wird empfohlen, diesen, sofern möglich, anstelle der structured_task_group-Klasse zu verwenden. Der parallel_invoke-Algorithmus wird im Thema Parallele Algorithmen ausführlicher beschrieben.
Verwenden Sie parallel_invoke, um mehrere unabhängige Aufgaben gleichzeitig auszuführen und sofort darauf zu warten, dass alle Aufgaben abgeschlossen sind. Verwenden Sie task_group, um mehrere unabhängige Aufgaben gleichzeitig auszuführen und später darauf zu warten, dass allle Aufgaben abgeschlossen sind. Beispielsweise können Sie einem task_group-Objekt Aufgaben hinzufügen und in einer anderen Funktion oder einem anderen Thread darauf warten, dass die Aufgaben beendet werden.
Aufgabengruppen unterstützen das Konzept eines Abbruchs. Mit einem Abbruch können Sie für alle aktiven Aufgaben angeben, dass der gesamte Vorgang abgebrochen werden soll. Durch den Abbruch wird außerdem verhindert, dass Aufgaben gestartet werden, die noch nicht gestartet wurden. Weitere Informationen über das Abbrechen finden Sie unter Abbruch in der PPL.
Die Laufzeit stellt außerdem ein Modell für die Ausnahmebehandlung bereit, mit dem Sie eine Ausnahme für eine Aufgabe auslösen und behandeln können, während Sie darauf warten, das die zugeordnete Aufgabengruppe fertig gestellt wird. Weitere Informationen zu diesem Modell für die Behandlung von Ausnahmen finden Sie unter Ausnahmebehandlung in der Concurrency Runtime.
task_group und structured_task_group im Vergleich
Grundsätzlich wird die Verwendung von task_group oder parallel_invoke anstelle der structured_task_group-Klasse empfohlen. In Einzelfällen, beispielsweise beim Schreiben eines parallelen Algorithmus für eine variable Anzahl von Aufgaben oder mit der Möglichkeit eines Abbruchs, können Sie jedoch structured_task_group verwenden. In diesem Abschnitt werden die Unterschiede zwischen der task_group-Klasse und der structured_task_group-Klasse erläutert.
Die task_group-Klasse ist threadsicher. Sie können einem task_group-Objekt daher Aufgaben von mehreren Threads hinzufügen und von mehreren Threads auf ein task_group-Objekt warten oder dieses abbrechen. Das Erstellen und Zerstören eines structured_task_group-Objekts muss im gleichen lexikalischen Gültigkeitsbereich erfolgen. Darüber hinaus müssen alle Vorgänge für ein structured_task_group-Objekt im gleichen Thread ausgeführt werden. Ausnahmen dieser Regel stellen die Concurrency::structured_task_group::cancel- und die Concurrency::structured_task_group::is_canceling-Methode dar. Eine untergeordnete Aufgabe kann diese Methoden aufrufen, um die übergeordnete Aufgabengruppe abzubrechen oder das Abbrechen jederzeit zu überprüfen.
Nachdem Sie die Concurrency::task_group::wait-Methode oder die Concurrency::task_group::run_and_wait-Methode aufgerufen haben, können Sie zusätzliche Aufgaben für ein task_group-Objekt ausführen. Sie können jedoch keine zusätzlichen Aufgaben für ein structured_task_group-Objekt ausführen, wenn Sie zunächst die Concurrency::structured_task_group::wait-Methode oder die Concurrency:: structured_task_group::run_and_wait-Methode aufrufen.
Da die structured_task_group-Klasse nicht threadübergreifend synchronisiert, ist ihr Ausführungsaufwand im Vergleich zur task_group-Klasse geringer. Wenn die Planung von Arbeit für mehrere Threads nicht Teil eines Problems ist und der parallel_invoke-Algorithmus nicht verwendet werden kann, können Sie mit der structured_task_group-Klasse leistungsfähigeren Code schreiben.
Wenn Sie ein structured_task_group-Objekt in einem anderen structured_task_group-Objekt verwenden, muss das innere Objekt abgeschlossen und zerstört sein, bevor das äußere Objekt beendet wird. Bei der task_group-Klasse ist die Fertigstellung geschachtelter Aufgabengruppen vor der äußeren Gruppe nicht erforderlich.
Unstrukturierte Aufgabengruppen und strukturierte Aufgabengruppen verwenden Aufgabenhandles auf unterschiedliche Weise. Sie können Arbeitsfunktionen direkt an ein task_group-Objekt übergeben; das Aufgabenhandle wird unmittelbar vom task_group-Objekt für Sie erstellt und verwaltet. Die structured_task_group-Klasse erfordert die Verwaltung eines task_handle-Objekts für jede Aufgabe. Jedes task_handle-Objekt muss über die gesamte Lebensdauer des zugeordneten structured_task_group-Objekts hinweg gültig sein. Erstellen Sie mit der Concurrency::make_task-Funktion ein task_handle-Objekt, wie im folgenden grundlegenden Beispiel veranschaulicht:
// make-task-structure.cpp
// compile with: /EHsc
#include <ppl.h>
using namespace Concurrency;
int wmain()
{
// Use the make_task function to define several tasks.
auto task1 = make_task([] { /*TODO: Define the task body.*/ });
auto task2 = make_task([] { /*TODO: Define the task body.*/ });
auto task3 = make_task([] { /*TODO: Define the task body.*/ });
// Create a structured task group and run the tasks concurrently.
structured_task_group tasks;
tasks.run(task1);
tasks.run(task2);
tasks.run_and_wait(task3);
}
Mit einer Routine für die Stapelzuweisung wie _malloca oder einer Containerklasse wie std::vector können Sie Aufgabenhandles für eine variable Anzahl von Aufgaben verwalten.
task_group und structured_task_group unterstützen die Möglichkeit eines Abbruchs. Weitere Informationen über das Abbrechen finden Sie unter Abbruch in der PPL.
Beispiel
Im folgenden grundlegenden Beispiel wird die Verwendung von Aufgabengruppen veranschaulicht. In diesem Beispiel werden vom parallel_invoke-Algorithmus zwei Aufgaben gleichzeitig ausgeführt. In jeder Aufgabe werden einem task_group-Objekt untergeordnete Aufgaben hinzugefügt. Die task_group-Klasse ermöglicht das zeitgleiche Hinzufügen für mehrere Aufgaben.
// using-task-groups.cpp
// compile with: /EHsc
#include <ppl.h>
#include <sstream>
#include <iostream>
using namespace Concurrency;
using namespace std;
// Prints a message to the console.
template<typename T>
void print_message(T t)
{
wstringstream ss;
ss << L"Message from task: " << t << endl;
wcout << ss.str();
}
int wmain()
{
// A task_group object that can be used from multiple threads.
task_group tasks;
// Concurrently add several tasks to the task_group object.
parallel_invoke(
[&] {
// Add a few tasks to the task_group object.
tasks.run([] { print_message(L"Hello"); });
tasks.run([] { print_message(42); });
},
[&] {
// Add one additional task to the task_group object.
tasks.run([] { print_message(3.14); });
}
);
// Wait for all tasks to finish.
tasks.wait();
}
Nachfolgend wird eine Beispielausgabe für dieses Beispiel angezeigt:
Message from task: Hello
Message from task: 3.14
Message from task: 42
Da die Aufgaben vom parallel_invoke-Algorithmus gleichzeitig ausgeführt werden, kann sich die Reihenfolge der Ausgabemeldungen unterscheiden.
Umfassende Beispiele zur Verwendung des parallel_invoke-Algorithmus finden Sie unter Gewusst wie: Verwenden von parallel_invoke zum Schreiben einer Runtime für paralleles Sortieren und Gewusst wie: Ausführen von parallelen Vorgängen mithilfe von parallel_invoke. Ein vollständiges Beispiel zur Implementierung asynchroner Futures mit der task_group-Klasse finden Sie unter Exemplarische Vorgehensweise: Implementieren von Futures.
Stabile Programmierung
Es ist wichtig, dass Sie die Rolle des Abbruchs und der Ausnahmebehandlung verstehen, wenn Sie Aufgabengruppen und parallele Algorithmen verwenden. Beispielweise kann eine abgebrochene Aufgabe in einer Struktur paralleler Arbeitsaufgaben dazu führen, dass untergeordnete Aufgaben nicht ausgeführt werden. Dies kann Probleme verursachen, wenn eine der untergeordneten Aufgaben einen Vorgang ausführen soll, der für die Anwendung von Bedeutung ist, beispielsweise das Freigeben einer Ressource. Wenn eine untergeordnete Aufgabe eine Ausnahme auslöst, kann diese Ausnahme außerdem über einen Objektdestruktor weitergeben werden und nicht definiertes Verhalten in der Anwendung auslösen. Ein Beispiel, in dem diese Punkte veranschaulicht werden, finden Sie im Abschnitt Informieren Sie sich über die Auswirkungen von Abbruch und Ausnahmebehandlung auf die Zerstörung von Objekten der empfohlenen Vorgehensweisen im Dokument zur Parallel Patterns Library. Weitere Informationen zur Ausnahmebehandlung sowie zu Abbruchmodellen in der PPL finden Sie unter Abbruch in der PPL und Ausnahmebehandlung in der Concurrency Runtime.
Verwandte Themen
Gewusst wie: Verwenden von parallel_invoke zum Schreiben einer Runtime für paralleles Sortieren
Erläutert, wie die Leistung des bitonischen Sortieralgorithmus mit dem parallel_invoke-Algorithmus verbessert werden.Gewusst wie: Ausführen von parallelen Vorgängen mithilfe von parallel_invoke
Erläutert, wie die Leistung eines Programms mit dem parallel_invoke-Algorithmus verbessert werden kann, das mehrere Vorgänge in einer freigegebenen Datenquelle ausführt.Exemplarische Vorgehensweise: Implementieren von Futures
Zeigt, wie die vorhandene Funktionalität in der Concurrency Runtime kombiniert werden kann, um mehr Funktionalität zu erreichen.Parallel Patterns Library (PPL)
Beschreibt die PPL, die ein obligatorisches Programmiermodell zum Entwickeln gleichzeitiger Anwendungen bereitstellt.
Verweise
Änderungsprotokoll
Datum |
Versionsgeschichte |
Grund |
---|---|---|
März 2011 |
Informationen zur Rolle von Abbrüchen und Ausnahmebehandlungen bei Aufgabengruppen und parallelen Algorithmen hinzugefügt. |
Informationsergänzung. |
Juli 2010 |
Inhalt neu strukturiert. |
Informationsergänzung. |
Mai 2010 |
Richtlinien erweitert. |
Informationsergänzung. |