Share via


Aufgabenparallelität (Concurrency Runtime)

In der Concurrency Runtime ist eine Aufgabe eine Arbeitseinheit, die einen bestimmten Auftrag ausführt und normalerweise parallel zu anderen Aufgaben ausgeführt wird. Eine Aufgabe kann in weitere, differenziertere Aufgaben zerlegt werden, die in einer Aufgabengruppe organisiert werden.

Sie verwenden Aufgaben, wenn Sie asynchronen Code schreiben und ein Vorgang erst ausgeführt werden soll, nachdem der asynchrone Vorgang abgeschlossen ist. Sie können beispielsweise eine Aufgabe verwenden, um asynchron aus einer Datei zu lesen, und dann eine andere Aufgabe – eine Fortsetzungsaufgabe, die später in diesem Dokument erläutert wird – zum Verarbeiten der Daten, nachdem sie verfügbar sind. Umgekehrt können Sie Aufgabengruppen verwenden, um parallele Arbeitsvorgänge in kleinere Teile zu zerlegen. Nehmen Sie zum Beispiel einmal an, dass Sie über einen rekursiven Algorithmus verfügen, der die verbleibende Arbeit in zwei Partitionen unterteilt. Sie können Aufgabengruppen verwenden, um diese Partitionen gleichzeitig auszuführen, und dann warten, bis die aufgeteilte Arbeit abgeschlossen ist.

Tipp

Wenn Sie die gleiche Routine parallel auf jedes Element einer Auflistung anwenden möchten, verwenden Sie parallele Algorithmen wie concurrency::parallel_for statt einer Aufgabe oder einer Aufgabengruppe.Weitere Informationen zu parallelen Algorithmen finden Sie unter Parallele Algorithmen.

Wesentliche Punkte

  • Wenn Sie Variablen als Verweis an einen Lambda-Ausdruck übergeben, müssen Sie sicherstellen, dass die Lebensdauer dieser Variablen bis zum Beenden der Aufgabe erhalten bleibt.

  • Verwenden Sie Aufgaben (die concurrency::task-Klasse), wenn Sie asynchronen Code schreiben.

  • Verwenden Sie Aufgabengruppen (die concurrency::task_group-Klasse oder den concurrency::parallel_invoke-Algorithmus), wenn Sie parallele Arbeitsvorgänge in kleinere Schritte zerlegen möchten, und warten Sie dann, bis diese kleineren Schritte abgeschlossen sind.

  • Verwenden Sie die concurrency::task::then-Methode , um Fortsetzungen zu erstellen. Eine Fortsetzung ist eine Aufgabe, die nach Abschluss einer anderen Aufgabe asynchron ausgeführt wird. Sie können eine beliebige Anzahl an Fortsetzungen verbinden, um eine Kette asynchroner Arbeitsvorgänge zu bilden.

  • Die Ausführung einer aufgabenbasierten Fortsetzung wird immer für den Zeitpunkt geplant, zu dem die Vorgängeraufgabe abgeschlossen ist, auch wenn die Vorgängeraufgabe abgebrochen wird oder wenn diese eine Ausnahme auslöst.

  • Verwenden Sie concurrency::when_all, um eine Aufgabe zu erstellen, die abgeschlossen wird, nachdem alle Mitglieder einer Gruppe von Aufgaben abgeschlossen sind. Verwenden Sie concurrency::when_any, um eine Aufgabe zu erstellen, die abgeschlossen wird, nachdem ein Mitglied einer Gruppe von Aufgaben abgeschlossen ist.

  • Für Aufgaben und Aufgabengruppen kann der Abbruchmechanismus der Parallel Patterns Library (PPL) verwendet werden. Weitere Informationen finden Sie unter Abbruch in der PPL.

  • Informationen dazu, wie in der Concurrency Runtime Ausnahmen behandelt werden, die von Aufgaben und Aufgabengruppen ausgelöst werden, finden Sie unter Ausnahmebehandlung in der Concurrency Runtime.

In diesem Dokument

  • Verwenden von Lambda-Ausdrücken

  • Die task-Klasse

  • Fortsetzungsaufgaben

  • Wertbasierte und aufgabenbasierte Fortsetzungen

  • Verfassen von Aufgaben

    • Die Funktion "when_all"

    • Die Funktion "when_any"

  • Verzögerte Aufgabenausführung

  • Aufgabengruppen

  • task_group und structured_task_group im Vergleich

  • Beispiel

  • Stabile Programmierung

Verwenden von Lambda-Ausdrücken

Aufgrund ihrer kompakten Syntax werden Lambda-Ausdrücke häufig zur Definition der Arbeit verwendet, die von Aufgaben und Aufgabengruppen ausgeführt wird. Im Folgenden finden Sie einige Verwendungstipps:

  • Da Aufgaben in der Regel in Hintergrundthreads ausgeführt werden, beachten Sie die Objektlebensdauer, wenn Sie Variablen in Lambda-Ausdrücken erfassen. Wenn Sie eine Variable als Wert erfassen, wird eine Kopie dieser Variablen im Lambda-Text erstellt. Wenn Sie sie als Verweis erfassen, wird keine Kopie erstellt. Daher müssen Sie sicherstellen, dass die Lebensdauer jeder Variablen, die Sie als Verweis erfassen, länger ist als die Lebensdauer der Aufgabe, die diese verwendet.

  • Wenn Sie einen Lambda-Ausdruck an eine Aufgabe übergeben, erfassen Sie keine Variablen, die auf dem Stapel als Verweis zugeordnet sind.

  • Bezeichnen Sie die in Lambda-Ausdrücken erfassten Variablen eindeutig, damit Sie feststellen können, welche Variablen Sie als Wert und welche Sie als Verweis erfassen. Aus diesem Grund wird empfohlen, die Option [=] oder [&] für Lambda-Ausdrücke nicht zu verwenden.

Häufig wird in einer Aufgabe in einer Fortsetzungskette eine Zuweisung zu einer Variablen vorgenommen und in einer anderen Aufgabe diese Variable gelesen. Sie können die Variable nicht als Wert erfassen, da jede Fortsetzungsaufgabe über eine andere Kopie der Variablen verfügen würde. Bei auf dem Stapel zugeordneten Variablen können Sie diese auch nicht als Verweis erfassen, da die Variable möglicherweise nicht mehr gültig ist.

Verwenden Sie zum Lösen dieses Problems einen intelligenten Zeiger, beispielsweise std::shared_ptr, um die Variable zu umschließen, und übergeben Sie den intelligenten Zeiger als Wert. Auf diese Weise kann eine Zuweisung zum zugrunde liegenden Objekt erfolgen, und es kann aus diesem Objekt gelesen werden. Außerdem ist seine Lebensdauer länger als die der Aufgaben, die es verwenden. Verwenden Sie diese Methode auch, wenn die Variable ein Zeiger oder ein Handle mit Verweiszählung (^) für ein Windows-Runtime-Objekt ist. Es folgt ein einfaches Beispiel:

// lambda-task-lifetime.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
#include <string>

using namespace concurrency;
using namespace std;

task<wstring> write_to_string()
{
    // Create a shared pointer to a string that is  
    // assigned to and read by multiple tasks. 
    // By using a shared pointer, the string outlives 
    // the tasks, which can run in the background after 
    // this function exits.
    auto s = make_shared<wstring>(L"Value 1");

    return create_task([s] 
    {
        // Print the current value.
        wcout << L"Current value: " << *s << endl;
        // Assign to a new value.
        *s = L"Value 2";

    }).then([s] 
    {
        // Print the current value.
        wcout << L"Current value: " << *s << endl;
        // Assign to a new value and return the string.
        *s = L"Value 3";
        return *s;
    });
}

int wmain()
{
    // Create a chain of tasks that work with a string.
    auto t = write_to_string();

    // Wait for the tasks to finish and print the result.
    wcout << L"Final value: " << t.get() << endl;
}

/* Output:
    Current value: Value 1
    Current value: Value 2
    Final value: Value 3
*/

Weitere Informationen zu Lambdaausdrücken finden Sie unter Lambda-Ausdrücke in C++.

[Nach oben]

Die task-Klasse

Sie können die concurrency::task-Klasse verwenden, um Aufgaben zu einem Satz abhängiger Vorgänge zu kombinieren. Dieses Kompositionsmodell wird durch das Konzept der Fortsetzungen unterstützt. Eine Fortsetzung ermöglicht das Ausführen von Code, wenn die vorherige Aufgabe, der Vorgänger, abgeschlossen ist. Das Ergebnis der Vorgängeraufgabe wird als Eingabe an eine oder mehrere Fortsetzungsaufgaben übergeben. Wenn eine Vorgängeraufgabe abgeschlossen wird, werden alle Fortsetzungsaufgaben, die darauf warten, für die Ausführung geplant. Jede Fortsetzungsaufgabe erhält eine Kopie des Ergebnisses der Vorgängeraufgabe. Diese Fortsetzungsaufgaben wiederum können auch Vorgängeraufgaben für andere Fortsetzungen sein, sodass sie eine Kette von Aufgaben bilden. Mit Fortsetzungen können Sie Ketten von Aufgaben beliebiger Länge erstellen, die bestimmte Abhängigkeiten untereinander aufweisen. Außerdem kann für eine Aufgabe der Abbruchmechanismus verwendet werden – entweder vor dem Start einer Aufgabe oder in kooperativer Weise, während die Aufgabe ausgeführt wird. Weitere Informationen zu diesem Abbruchmodell finden Sie unter Abbruch in der PPL.

Bei task handelt es sich um eine Vorlagenklasse. Der Typparameter T gibt den Typ des Ergebnisses an, das von der Aufgabe erzeugt wird. Dieser Typ kann void sein, wenn die Aufgabe keinen Wert zurückgibt. Für T kann der const-Modifizierer nicht verwendet werden.

Beim Erstellen einer Aufgabe stellen Sie eine Arbeitsfunktion bereit, die den Aufgabentext ausführt. Bei dieser Arbeitsfunktion kann es sich um eine Lambda-Funktion, einen Funktionszeiger oder ein Funktionsobjekt handeln. Sie können auf die Beendigung einer Aufgabe warten, ohne das Ergebnis abzurufen, indem Sie die concurrency::task::wait-Methode aufrufen. Die task::wait-Methode gibt einen concurrency::task_status-Wert zurück, der angibt, ob die Aufgabe abgeschlossen oder abgebrochen wurde. Rufen Sie zum Abrufen des Ergebnisses der Aufgabe die concurrency::task::get-Methode auf. Von dieser Methode wird task::wait aufgerufen, um darauf zu warten, dass die Aufgabe beendet wird. Daher wird die Ausführung des aktuellen Threads blockiert, bis das Ergebnis zur Verfügung steht.

Im folgenden Beispiel wird gezeigt, wie eine Aufgabe erstellt, auf das Ergebnis gewartet und dessen Wert angezeigt wird. In den Beispielen in dieser Dokumentation werden Lambda-Funktionen verwendet, da sie eine kompaktere Syntax aufweisen. Sie können bei der Verwendung von Aufgaben jedoch auch Funktionszeiger und Funktionsobjekte verwenden.

// basic-task.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Create a task.
    task<int> t([]()
    {
        return 42;
    });

    // In this example, you don't necessarily need to call wait() because 
    // the call to get() also waits for the result.
    t.wait();

    // Print the result.
    wcout << t.get() << endl;
}

/* Output:
    42
*/

Bei der Verwendung der concurrency::create_task-Funktion können Sie das Schlüsselwort auto verwenden, statt den Typ zu deklarieren. Betrachten Sie beispielsweise diesen Code, mit dem die Identitätsmatrix erstellt und ausgegeben wird:

// create-task.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <string>
#include <iostream>
#include <array>

using namespace concurrency;
using namespace std;

int wmain()
{
    task<array<array<int, 10>, 10>> create_identity_matrix([]
    {
        array<array<int, 10>, 10> matrix;
        int row = 0;
        for_each(begin(matrix), end(matrix), [&row](array<int, 10>& matrixRow) 
        {
            fill(begin(matrixRow), end(matrixRow), 0);
            matrixRow[row] = 1;
            row++;
        });
        return matrix;
    });

    auto print_matrix = create_identity_matrix.then([](array<array<int, 10>, 10> matrix)
    {
        for_each(begin(matrix), end(matrix), [](array<int, 10>& matrixRow) 
        {
            wstring comma;
            for_each(begin(matrixRow), end(matrixRow), [&comma](int n) 
            {
                wcout << comma << n;
                comma = L", ";
            });
            wcout << endl;
        });
    });

    print_matrix.wait();
}
/* Output:
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0
    0, 1, 0, 0, 0, 0, 0, 0, 0, 0
    0, 0, 1, 0, 0, 0, 0, 0, 0, 0
    0, 0, 0, 1, 0, 0, 0, 0, 0, 0
    0, 0, 0, 0, 1, 0, 0, 0, 0, 0
    0, 0, 0, 0, 0, 1, 0, 0, 0, 0
    0, 0, 0, 0, 0, 0, 1, 0, 0, 0
    0, 0, 0, 0, 0, 0, 0, 1, 0, 0
    0, 0, 0, 0, 0, 0, 0, 0, 1, 0
    0, 0, 0, 0, 0, 0, 0, 0, 0, 1
*/

Sie können die create_task-Funktion verwenden, um den entsprechenden Vorgang zu erstellen.

auto create_identity_matrix = create_task([]
{
    array<array<int, 10>, 10> matrix;
    int row = 0;
    for_each(begin(matrix), end(matrix), [&row](array<int, 10>& matrixRow) 
    {
        fill(begin(matrixRow), end(matrixRow), 0);
        matrixRow[row] = 1;
        row++;
    });
    return matrix;
});

Wenn während der Ausführung einer Aufgabe eine Ausnahme ausgelöst wird, wird die Ausnahme von der Laufzeit im nachfolgenden Aufruf an task::get, task::wait oder eine aufgabenbasierte Fortsetzung gemarshallt. Weitere Informationen zu dem Mechanismus für die Behandlung von Ausnahmen in Aufgaben finden Sie unter Ausnahmebehandlung in der Concurrency Runtime.

Ein Beispiel, in dem task, concurrency::task_completion_event und ein Abbruch verwendet werden, finden Sie unter Exemplarische Vorgehensweise: Verbinden von Verwendungsaufgaben und XML-HTTP-Anforderungen. (Die task_completion_event-Klasse wird weiter unten in diesem Dokument beschrieben.)

Tipp

Spezifische Details zu Aufgaben in Windows Store-Apps finden Sie unter Asynchronous programming in C++ und Erstellen von asynchronen Vorgängen in C++ für Windows Store-Apps.

[Nach oben]

Fortsetzungsaufgaben

Bei der asynchronen Programmierung werden nach Abschluss eines asynchronen Vorgangs häufig ein zweiter Vorgang aufgerufen und Daten an diesen weitergegeben. Herkömmlicherweise werden hierfür Rückrufmethoden verwendet. In der Concurrency Runtime wird die gleiche Funktionalität durch Fortsetzungsaufgaben bereitgestellt. Eine Fortsetzungsaufgabe (auch kurz als Fortsetzung bezeichnet) ist eine asynchrone Aufgabe, die von einer anderen Aufgabe, die wiederum als Vorgänger bezeichnet wird, nach deren Abschluss aufgerufen wird. Mithilfe von Fortsetzungen können Sie folgende Aufgaben ausführen:

  • Übergeben von Daten vom Vorgänger an die Fortsetzung

  • Angeben der präzisen Bedingungen, unter denen die Fortsetzung aufgerufen bzw. nicht aufgerufen wird

  • Abbrechen einer Fortsetzung, bevor diese gestartet wird oder kooperativ während sie ausgeführt wird

  • Bereitstellen von Hinweisen zur Planung der Fortsetzung (Dies gilt ausschließlich für Windows Store-Apps. Weitere Informationen finden Sie unter Erstellen von asynchronen Vorgängen in C++ für Windows Store-Apps.)

  • Aufrufen mehrerer Fortsetzungen durch den gleichen Vorgänger

  • Aufrufen einer Fortsetzung, wenn alle Vorgänger oder einer der Vorgänger abgeschlossen wird

  • Verketten von Fortsetzungen auf eine beliebige Länge

  • Behandeln von durch den Vorgänger ausgelöste Ausnahmen mithilfe einer Fortsetzung

Mithilfe dieser Funktionen können Sie eine oder mehrere Aufgaben ausführen, wenn die erste Aufgabe abgeschlossen wird. Sie können beispielsweise eine Fortsetzung erstellen, in der eine Datei komprimiert wird, nachdem sie von der ersten Aufgabe vom Datenträger gelesen wurde.

Im folgenden Beispiel wird das vorherige so geändert, dass die concurrency::task::then-Methode zum Planen einer Fortsetzung verwendet wird, die den Wert der Vorgängeraufgabe ausgibt, wenn dieser zur Verfügung steht.

// basic-continuation.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t = create_task([]() -> int
    {
        return 42;
    });

    t.then([](int result)
    {
        wcout << result << endl;
    }).wait();

    // Alternatively, you can chain the tasks directly and 
    // eliminate the local variable. 
    /*create_task([]() -> int
    {
        return 42;
    }).then([](int result)
    {
        wcout << result << endl;
    }).wait();*/
}

/* Output:
    42
*/

Sie können Aufgaben auf eine beliebige Länge verketten und schachteln. Eine Aufgabe kann auch über mehrere Fortsetzungen verfügen. Im folgenden Beispiel wird eine einfache Fortsetzungskette dargestellt, in der der Wert der vorherigen Aufgabe dreimal erhöht wird.

// continuation-chain.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t = create_task([]() -> int
    { 
        return 0;
    });

    // Create a lambda that increments its input value.
    auto increment = [](int n) { return n + 1; };

    // Run a chain of continuations and print the result. 
    int result = t.then(increment).then(increment).then(increment).get();
    wcout << result << endl;
}

/* Output:
    3
*/

Eine Fortsetzung kann auch eine andere Aufgabe zurückgeben. Wenn kein Abbruch erfolgt, wird diese Aufgabe vor der nachfolgenden Fortsetzung ausgeführt. Diese Technik wird als asynchrones Entpacken bezeichnet. Das asynchrone Entpacken ist nützlich, wenn Sie zusätzliche Arbeitsvorgänge im Hintergrund ausführen möchten, jedoch nicht möchten, dass der aktuelle Thread durch die aktuelle Aufgabe blockiert wird. (Dies ist in Windows Store-Apps häufig der Fall, in denen Fortsetzungen im UI-Thread ausgeführt werden können.) Im folgenden Beispiel werden drei Aufgaben gezeigt. Die erste Aufgabe gibt eine andere Aufgabe zurück, die vor einer Fortsetzungsaufgabe ausgeführt wird.

// async-unwrapping.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t = create_task([]()
    {
        wcout << L"Task A" << endl;

        // Create an inner task that runs before any continuation 
        // of the outer task. 
        return create_task([]()
        {
            wcout << L"Task B" << endl;
        });
    });

    // Run and wait for a continuation of the outer task.
    t.then([]()
    {
        wcout << L"Task C" << endl;
    }).wait();
}

/* Output:
    Task A
    Task B
    Task C
*/

Wichtig

Wenn eine Fortsetzung einer Aufgabe eine geschachtelte Aufgabe vom Typ N zurückgibt, ist die resultierende Aufgabe vom Typ N, nicht vom Typ task<N>, und wird abgeschlossen, wenn die geschachtelte Aufgabe abgeschlossen wird.Das heißt, die Fortsetzung entpackt die geschachtelte Aufgabe.

[Nach oben]

Wertbasierte und aufgabenbasierte Fortsetzungen

Bei einem task-Objekt, dessen Rückgabetyp T ist, können Sie einen Wert des Typs T oder task<T> für die zugehörigen Fortsetzungsaufgaben bereitstellen. Eine Fortsetzung, die den Typ T akzeptiert, wird als wertbasierte Fortsetzung bezeichnet. Eine wertbasierte Fortsetzung wird für die Ausführung geplant, wenn die Vorgängeraufgabe ohne Fehler abgeschlossen und nicht abgebrochen wird. Eine Fortsetzung, die den Typ task<T> als Parameter akzeptiert, wird als aufgabenbasierte Fortsetzung bezeichnet. Die Ausführung einer aufgabenbasierten Fortsetzung wird immer für den Zeitpunkt geplant, zu dem die Vorgängeraufgabe abgeschlossen ist, auch wenn die Vorgängeraufgabe abgebrochen wird oder wenn diese eine Ausnahme auslöst. Sie können dann task::get aufrufen, um das Ergebnis der Vorgängeraufgabe abzurufen. Wenn die Vorgängeraufgabe abgebrochen wurde, löst task::getconcurrency::task_canceled aus. Wenn von der Vorgängeraufgabe eine Ausnahme ausgelöst wurde, wird von task::get diese Ausnahme erneut ausgelöst. Eine aufgabenbasierte Fortsetzung wird nicht als abgebrochen markiert, wenn die zugehörige Vorgängeraufgabe abgebrochen wird.

[Nach oben]

Verfassen von Aufgaben

In diesem Abschnitt werden die concurrency::when_all-Funktion und die concurrency::when_any-Funktion beschrieben, mit denen Sie mehrere Aufgaben zusammenstellen können, um allgemeine Muster zu implementieren.

Die Funktion "when_all"

Von der when_all-Funktion wird eine Aufgabe erstellt, die abgeschlossen wird, nachdem ein Satz von Aufgaben abgeschlossen wurde. Diese Funktion gibt ein std::vector-Objekt zurück, das die Ergebnisse der einzelnen Aufgaben im Satz enthält. Im folgenden einfachen Beispiel wird mithilfe von when_all eine Aufgabe erstellt, die den Abschluss von drei anderen Aufgaben darstellt.

// join-tasks.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <array>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Start multiple tasks. 
    array<task<void>, 3> tasks = 
    {
        create_task([] { wcout << L"Hello from taskA." << endl; }),
        create_task([] { wcout << L"Hello from taskB." << endl; }),
        create_task([] { wcout << L"Hello from taskC." << endl; })
    };

    auto joinTask = when_all(begin(tasks), end(tasks));

    // Print a message from the joining thread.
    wcout << L"Hello from the joining thread." << endl;

    // Wait for the tasks to finish.
    joinTask.wait();
}

/* Sample output:
    Hello from the joining thread.
    Hello from taskA.
    Hello from taskC.
    Hello from taskB.
*/

Hinweis

Die Aufgaben, die Sie an when_all übergeben, müssen einheitlich sein.Das heißt, sie müssen alle den gleichen Typ zurückgeben.

Sie können auch die Syntax && verwenden, um eine Aufgabe zu erstellen, die nach Abschluss eines Satzes von Aufgaben abgeschlossen wird, wie im folgenden Beispiel gezeigt.

auto t = t1 && t2; // same as when_all

Es ist üblich, eine Fortsetzung zusammen mit when_all zu verwenden, um eine Aktion auszuführen, nachdem ein Satz von Aufgaben abgeschlossen wurde. Im folgenden Beispiel wird das vorherige so geändert, dass die Summe von drei Aufgaben ausgegeben wird, die jeweils ein Ergebnis vom Typ int liefern.

// Start multiple tasks. 
array<task<int>, 3> tasks =
{
    create_task([]() -> int { return 88; }),
    create_task([]() -> int { return 42; }),
    create_task([]() -> int { return 99; })
};

auto joinTask = when_all(begin(tasks), end(tasks)).then([](vector<int> results)
{
    wcout << L"The sum is " 
          << accumulate(begin(results), end(results), 0)
          << L'.' << endl;
});

// Print a message from the joining thread.
wcout << L"Hello from the joining thread." << endl;

// Wait for the tasks to finish.
joinTask.wait();

/* Output:
    Hello from the joining thread.
    The sum is 229.
*/

In diesem Beispiel können Sie auch task<vector<int>> angeben, um eine aufgabenbasierte Fortsetzung zu erstellen.

Wenn eine Aufgabe in einem Satz von Aufgaben abgebrochen wird oder eine Ausnahme auslöst, wird when_all sofort abgeschlossen und wartet nicht, bis die übrigen Aufgaben beendet sind. Wenn eine Ausnahme ausgelöst wird, löst die Laufzeit die Ausnahme erneut aus, wenn Sie task::get oder task::wait für das Task-Objekt aufrufen, das von when_all zurückgegeben wird. Wenn von mehr als einer Aufgabe eine Ausnahme ausgelöst wird, wird von der Laufzeit eine ausgewählt. Daher müssen Sie sicherstellen, dass Sie alle Ausnahmen nach Abschluss aller Aufgaben berücksichtigen. Eine nicht behandelte Ausnahme einer Aufgabe führt dazu, dass die App beendet wird.

Es steht eine Hilfsfunktion zur Verfügung, mit der Sie sicherstellen können, dass Ihr Programm alle Ausnahmen berücksichtigt. Die Funktion observe_all_exceptions löst für jede Aufgabe im bereitgestellten Bereich jede aufgetretene Ausnahme aus, damit diese erneut ausgelöst wird, und "schluckt" diese anschließend.

// Observes all exceptions that occurred in all tasks in the given range. 
template<class T, class InIt> 
void observe_all_exceptions(InIt first, InIt last) 
{
    std::for_each(first, last, [](concurrency::task<T> t)
    {
        t.then([](concurrency::task<T> previousTask)
        {
            try
            {
                previousTask.get();
            }
            // Although you could catch (...), this demonstrates how to catch specific exceptions. Your app 
            // might handle different exception types in different ways. 
            catch (Platform::Exception^)
            {
                // Swallow the exception.
            }
            catch (const std::exception&)
            {
                // Swallow the exception.
            }
        });
    });
}

Betrachten Sie eine Windows Store-App, in der C++ und XAML verwendet werden und mit der eine Reihe von Dateien auf einen Datenträger geschrieben wird. Im folgenden Beispiel wird gezeigt, wie when_all und observe_all_exceptions verwendet werden, um sicherzustellen, dass das Programm alle Ausnahmen berücksichtigt.

// Writes content to files in the provided storage folder. 
// The first element in each pair is the file name. The second element holds the file contents.
task<void> MainPage::WriteFilesAsync(StorageFolder^ folder, const vector<pair<String^, String^>>& fileContents)
{
    // For each file, create a task chain that creates the file and then writes content to it. Then add the task chain to a vector of tasks.
    vector<task<void>> tasks;
    for (auto fileContent : fileContents)
    {
        auto fileName = fileContent.first;
        auto content = fileContent.second;

        // Create the file. The CreationCollisionOption::FailIfExists flag specifies to fail if the file already exists.
        tasks.emplace_back(create_task(folder->CreateFileAsync(fileName, CreationCollisionOption::FailIfExists)).then([content](StorageFile^ file)
        {
            // Write its contents. 
            return create_task(FileIO::WriteTextAsync(file, content));
        }));
    }

    // When all tasks finish, create a continuation task that observes any exceptions that occurred. 
    return when_all(begin(tasks), end(tasks)).then([tasks](task<void> previousTask)
    {
        task_status status = completed;
        try
        {
            status = previousTask.wait();
        }
        catch (COMException^ e)
        {
            // We'll handle the specific errors below.
        }
        // TODO: If other exception types might happen, add catch handlers here. 

        // Ensure that we observe all exceptions.
        observe_all_exceptions<void>(begin(tasks), end(tasks));

        // Cancel any continuations that occur after this task if any previous task was canceled. 
        // Although cancellation is not part of this example, we recommend this pattern for cases that do. 
        if (status == canceled)
        {
            cancel_current_task();
        }
    });
}

So führen Sie dieses Beispiel aus

  1. Fügen Sie in "MainPage.xaml" ein Button-Steuerelement hinzu.

    <Button x:Name="Button1" Click="Button_Click">Write files</Button>
    
  2. Fügen Sie in "MainPage.xaml.h" diese Vorwärtsdeklarationen zum Abschnitt private der MainPage-Klassendeklaration hinzu.

    void Button_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e);
    concurrency::task<void> WriteFilesAsync(Windows::Storage::StorageFolder^ folder, const std::vector<std::pair<Platform::String^, Platform::String^>>& fileContents);
    
  3. Implementieren Sie in "MainPage.xaml.cpp" den Button_Click-Ereignishandler.

    // A button click handler that demonstrates the scenario. 
    void MainPage::Button_Click(Object^ sender, RoutedEventArgs^ e)
    {
        // In this example, the same file name is specified two times. WriteFilesAsync fails if one of the files already exists.
        vector<pair<String^, String^>> fileContents;
        fileContents.emplace_back(make_pair(ref new String(L"file1.txt"), ref new String(L"Contents of file 1")));
        fileContents.emplace_back(make_pair(ref new String(L"file2.txt"), ref new String(L"Contents of file 2")));
        fileContents.emplace_back(make_pair(ref new String(L"file1.txt"), ref new String(L"Contents of file 3")));
    
        Button1->IsEnabled = false; // Disable the button during the operation.
        WriteFilesAsync(ApplicationData::Current->TemporaryFolder, fileContents).then([this](task<void> previousTask)
        {
            try
            {
                previousTask.get();
            }
            // Although cancellation is not part of this example, we recommend this pattern for cases that do. 
            catch (const task_canceled&)
            {
                // Your app might show a message to the user, or handle the error in some other way.
            }
    
            Button1->IsEnabled = true; // Enable the button.
        });
    }
    
  4. Implementieren Sie in "MainPage.xaml.cpp" WriteFilesAsync wie im Beispiel dargestellt.

Tipp

when_all ist eine nicht blockierende Funktion, die task als Ergebnis erzeugt.Anders als bei task::wait ist es sicher, diese Funktion in einer Windows Store-App auf dem ASTA-(Application STA-)Thread aufzurufen.

[Nach oben]

Die Funktion "when_any"

Die when_any-Funktion erstellt eine Aufgabe, die abgeschlossen wird, wenn die erste Aufgabe in einem Satz von Aufgaben abgeschlossen wird. Diese Funktion gibt ein std::pair-Objekt zurück, das das Ergebnis der abgeschlossenen Aufgabe und den Index dieser Aufgabe im Satz enthält.

Die when_any-Funktion ist insbesondere in folgenden Szenarien nützlich:

  • Redundante Vorgänge. Betrachten Sie einen Algorithmus oder einen Vorgang, der auf verschiedene Weise ausgeführt werden kann. Sie können die when_any-Funktion verwenden, um den Vorgang auszuwählen, der zuerst beendet wird, und dann die verbleibenden Vorgänge abzubrechen.

  • Überlappende Vorgänge. Sie können mehrere Vorgänge starten, die alle beendet werden müssen, und die when_any-Funktion verwenden, um Ergebnisse zu verarbeiten, wenn jeder Vorgang beendet wird. Nachdem ein Vorgang beendet wurde, können Sie eine oder mehrere weitere Aufgaben starten.

  • Eingeschränkte Vorgänge. Sie können die when_any-Funktion verwenden, um das vorherige Szenario zu erweitern, indem Sie die Anzahl der gleichzeitigen Vorgänge einschränken.

  • Abgelaufene Vorgänge. Sie können die when_any-Funktion verwenden, um zwischen einer oder mehreren Aufgaben und einer Aufgabe auszuwählen, die nach einer bestimmten Zeit beendet wird.

Wie bei when_all wird häufig eine Fortsetzung verwendet, in der mithilfe von when_any eine Aktion ausgeführt wird, wenn die erste Aufgabe in einem Satz von Aufgaben beendet wird. Im folgenden einfachen Beispiel wird mithilfe von when_any eine Aufgabe erstellt, die abgeschlossen wird, wenn die erste von drei anderen Aufgaben abgeschlossen wird.

// select-task.cpp 
// compile with: /EHsc
#include <ppltasks.h>
#include <array>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Start multiple tasks. 
    array<task<int>, 3> tasks = {
        create_task([]() -> int { return 88; }),
        create_task([]() -> int { return 42; }),
        create_task([]() -> int { return 99; })
    };

    // Select the first to finish.
    when_any(begin(tasks), end(tasks)).then([](pair<int, size_t> result)
    {
        wcout << "First task to finish returns "
              << result.first
              << L" and has index "
              << result.second
              << L'.' << endl;
    }).wait();
}

/* Sample output:
    First task to finish returns 42 and has index 1.
*/

In diesem Beispiel können Sie auch task<pair<int, size_t>> angeben, um eine aufgabenbasierte Fortsetzung zu erstellen.

Hinweis

Wie bei when_all müssen alle an when_any übergebenen Aufgaben denselben Typ zurückgeben.

Sie können auch die Syntax || verwenden, um eine Aufgabe zu erstellen, die nach der ersten Aufgabe in einem Satz von Aufgaben abgeschlossen wird, wie im folgenden Beispiel gezeigt.

auto t = t1 || t2; // same as when_any

Tipp

Wie bei when_all ist when_any nicht blockierend und kann sicher in einer Windows Store-App auf dem ASTA-Thread aufgerufen werden.

[Nach oben]

Verzögerte Aufgabenausführung

In einigen Fällen ist es notwendig, die Ausführung einer Aufgabe zu verzögern, bis eine Bedingung erfüllt ist, oder eine Aufgabe als Reaktion auf ein externes Ereignis zu starten. Bei der asynchronen Programmierung müssen Sie zum Beispiel möglicherweise eine Aufgabe als Reaktion auf ein E/A-Abschlussereignis starten.

Es gibt zwei Möglichkeiten, dies zu erreichen: Sie können eine Fortsetzung verwenden oder eine Aufgabe starten und auf ein Ereignis innerhalb der Arbeitsfunktion der Aufgabe warten. Allerdings gibt es Fälle, in denen es nicht möglich, eine dieser Techniken zu verwenden. Sie müssen beispielsweise über die Vorgängeraufgabe verfügen, um eine Fortsetzung zu erstellen. Wenn Sie jedoch nicht über die Vorgängeraufgabe verfügen, können Sie ein Aufgabenabschlussereignis erstellen und dieses Ereignis später mit der Vorgängeraufgabe verketten, wenn diese zur Verfügung steht. Da eine wartende Aufgabe auch einen Thread blockiert, können Sie Aufgabenabschlussereignisse außerdem dazu verwenden, Arbeitsvorgänge auszuführen, wenn ein asynchroner Vorgang abgeschlossen wird, und dadurch einen Thread freigeben.

Die concurrency::task_completion_event-Klasse vereinfacht eine solche Komposition von Aufgaben. Wie die task-Klasse ist der Typparameter T der Typ des Ergebnisses, das von der Aufgabe erzeugt wird. Dieser Typ kann void sein, wenn die Aufgabe keinen Wert zurückgibt. Für T kann der const-Modifizierer nicht verwendet werden. In der Regel wird ein task_completion_event-Objekt für einen Thread oder eine Aufgabe bereitgestellt, der bzw. die signalisieren, wenn der Wert für das Objekt zur Verfügung steht. Gleichzeitig wird mindestens eine Aufgabe als Listener dieses Ereignisses festgelegt. Wenn das Ereignis festgelegt wird, werden die Listeneraufgaben abgeschlossen und ihre Fortsetzungen für die Ausführung geplant.

Ein Beispiel, in dem task_completion_event zum Implementieren einer Aufgabe verwendet wird, die nach einer Verzögerung abgeschlossen wird, finden Sie unter Gewusst wie: Erstellen einer Aufgabe, die nach einer Verzögerung abgeschlossen wird.

[Nach oben]

Aufgabengruppen

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 die in diesen Gruppen ausgeführten Aufgaben werden mit der concurrency::task_handle-Klasse dargestellt. In der task_handle-Klasse wird der Code gekapselt, der die Arbeit ausführt. Wie die task-Klasse steht die Arbeitsfunktion in Form einer Lambda-Funktion, eines Funktionszeigers oder eines Funktionsobjekts zur Verfügung. 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. In der PPL werden unstrukturierte Aufgabengruppen mithilfe der task_group-Klasse und strukturierte Aufgabengruppen mithilfe der structured_task_group-Klasse dargestellt.

Wichtig

Außerdem wird in der PPL der concurrency::parallel_invoke-Algorithmus definiert, mit dem ein Satz von Aufgaben mithilfe der structured_task_group-Klasse parallel ausgeführt wird.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. Diese Technik wird häufig als Fork-Join-Parallelität bezeichnet. 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.

[Nach oben]

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 in 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 von 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. Wenn Sie hingegen zusätzliche Aufgaben für ein structured_task_group-Objekt ausführen, nachdem Sie die concurrency::structured_task_group::wait-Methode oder die concurrency:: structured_task_group::run_and_wait-Methode aufgerufen haben, ist das Verhalten nicht definiert.

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.

[Nach oben]

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:

  

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.

[Nach oben]

Stabile Programmierung

Es ist wichtig, dass Sie die Rolle des Abbruchs und der Ausnahmebehandlung verstehen, wenn Sie Aufgaben, 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.

[Nach oben]

Verwandte Themen

Titel

Beschreibung

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.

Gewusst wie: Erstellen einer Aufgabe, die nach einer Verzögerung abgeschlossen wird

Erläutert, wie mithilfe der Klassen task, cancellation_token_source, cancellation_token und task_completion_event eine Aufgabe erstellt wird, die nach einer Verzögerung abgeschlossen wird.

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.

Verweis

task-Klasse (Concurrency Runtime)

task_completion_event-Klasse

when_all-Funktion

when_any-Funktion

task_group-Klasse

parallel_invoke-Funktion

structured_task_group-Klasse