Aufgabenparallelität (Concurrency Runtime)

In der Parallelitätslaufzeit ist eine Aufgabe eine Arbeitseinheit, die einen bestimmten Auftrag ausführt und in der Regel parallel zu anderen Aufgaben ausgeführt wird. Eine Aufgabe kann in zusätzliche, feinkörnige Vorgänge aufgeteilt werden, die in einer Aufgabengruppe organisiert sind.

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 z. B. eine Aufgabe verwenden, um asynchron aus einer Datei zu lesen und dann eine weitere Aufgabe ( eine Fortsetzungsaufgabe, die später in diesem Dokument erläutert wird ) zu verwenden, um die Daten zu verarbeiten, nachdem sie verfügbar ist. 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 dieselbe Routine auf jedes Element einer Auflistung parallel anwenden möchten, verwenden Sie einen parallelen Algorithmus, z . B. parallele Parallelität::p arallel_for, anstelle einer Aufgabe oder Aufgabengruppe. Weitere Informationen zu parallelen Algorithmen finden Sie unter Parallel-Algorithmen.

Die wichtigsten Punkte

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

  • Verwenden Sie Aufgaben (die Parallelität::task-Klasse ), wenn Sie asynchronen Code schreiben. Die Aufgabenklasse verwendet den Windows-ThreadPool als zugehörigen Planer und nicht die Concurrency Runtime.

  • Verwenden Sie Aufgabengruppen (die Concurrency::task_group-Klasse oder den Parallelitätsalgorithmus::p arallel_invoke-Algorithmus ), wenn Sie parallele Arbeit in kleinere Teile dekompilieren möchten, und warten Sie dann, bis diese kleineren Teile abgeschlossen sind.

  • Verwenden Sie die Parallelität::task::then-Methode , um Fortsetzungen zu erstellen. Eine Fortsetzung ist eine Aufgabe, die asynchron ausgeführt wird, nachdem eine andere Aufgabe abgeschlossen wurde. 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 Parallelität::when_all , um eine Aufgabe zu erstellen, die nach Abschluss jedes Elements einer Gruppe von Aufgaben abgeschlossen wird. Verwenden Sie Parallelität::when_any , um eine Aufgabe zu erstellen, die nach abschluss eines Elements einer Reihe von Aufgaben abgeschlossen wird.

  • 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 die Laufzeit Ausnahmen verarbeitet, die von Aufgaben und Aufgabengruppen ausgelöst werden, finden Sie unter "Ausnahmebehandlung".

In diesem Dokument

Verwenden von Lambdaausdrü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 Lambdaausdrü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 im Stapel nach Verweis zugewiesen werden.

  • Geben Sie explizite Informationen zu den Variablen an, die Sie in Lambda-Ausdrücken erfassen, sodass Sie ermitteln können, was Sie nach Wert und nach 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 nicht nach Wert erfassen, da jede Fortsetzungsaufgabe eine andere Kopie der Variablen enthält. Bei vom Stapel zugewiesenen Variablen können Sie auch nicht anhand eines Verweises erfassen, da die Variable möglicherweise nicht mehr gültig ist.

Um dieses Problem zu lösen, verwenden Sie einen intelligenten Zeiger, z . B. "std::shared_ptr", um die Variable umzuschließen und den intelligenten Zeiger nach Wert zu übergeben. 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. Im Folgenden finden Sie 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 Expressions (Lambdaausdrücke).

Die Aufgabenklasse

Sie können die Parallelität::Task-Klasse verwenden, um Aufgaben in einer Reihe abhängiger Vorgänge zu verfassen. Dieses Kompositionsmodell wird durch den Begriff der Fortsetzungen unterstützt. Eine Fortsetzung ermöglicht die Ausführung von Code, wenn die vorherige oder vorgegangene Aufgabe 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.

Wenn Sie eine Aufgabe erstellen, 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. Um zu warten, bis eine Aufgabe abgeschlossen ist, ohne das Ergebnis zu erhalten, rufen Sie die Parallelität::task::wait-Methode auf. Die task::wait Methode gibt einen Parallelitätswert::task_status zurück, der beschreibt, ob die Aufgabe abgeschlossen oder abgebrochen wurde. Rufen Sie zum Abrufen des Ergebnisses der Aufgabe die Parallelität::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
*/

Wenn Sie die Funktion "concurrency::create_task" verwenden, können Sie die auto Schlüsselwort (keyword) verwenden, anstatt 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 zum Mechanismus für die Aufgaben-Ausnahmebehandlung finden Sie unter "Ausnahmebehandlung".

Ein Beispiel für die Verwendung von task"Parallelität::task_completion_event", "Abbruch" finden Sie unter "Walkthrough: Verbinden ing Using Tasks and XML HTTP Requests". (Die task_completion_event-Klasse wird weiter unten in diesem Dokument beschrieben.)

Tipp

Weitere Informationen zu Aufgaben in UWP-Apps finden Sie unter Asynchrone Programmierung in C++ und Erstellen asynchroner Vorgänge in C++ für UWP-Apps.

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 Parallelitätslaufzeit wird die gleiche Funktionalität von Fortsetzungsaufgaben bereitgestellt. Eine Fortsetzungsaufgabe (auch als Fortsetzung bezeichnet) ist eine asynchrone Aufgabe, die von einem anderen Vorgang aufgerufen wird, der als vorerkennter Vorgang bezeichnet wird, wenn die Vorstufe abgeschlossen ist. 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 nur für Universelle Windows-Plattform(UWP)-Apps. Weitere Informationen finden Sie unter Erstellen asynchroner Vorgänge in C++ für UWP-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 die vorherige Methode so geändert, dass die Parallelität::task::then-Methode verwendet wird, um eine Fortsetzung zu planen, die den Wert der vorerkennten Aufgabe druckt, wenn sie verfügbar ist.

// 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 UWP-Apps üblich, bei 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.

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 Typ T verwendet, 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 verwendet, 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 vorerkennte Aufgabe abgebrochen wurde, task::get wird die Parallelität::task_canceled ausgelöst. 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.

Verfassen von Vorgängen

In diesem Abschnitt werden die Parallelität::when_all und parallele Funktionen::when_any beschrieben, mit denen Sie mehrere Aufgaben erstellen können, um allgemeine Muster zu implementieren.

Die when_all-Funktion

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 das Ergebnis jeder Aufgabe 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.

Hier ist eine Hilfsfunktion, die Sie verwenden können, um sicherzustellen, dass Ihr Programm alle Ausnahmen beachtet. 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.
            }
        });
    });
}

Erwägen Sie eine UWP-App, die C++ und XAML verwendet, und schreibt eine Reihe von Dateien auf den Datenträger. 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>
  1. 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);
  1. 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.
    });
}
  1. Implementieren Sie in "MainPage.xaml.cpp" WriteFilesAsync wie im Beispiel dargestellt.

Tipp

when_all ist eine nicht blockierende Funktion, die task als Ergebnis erzeugt. Im Gegensatz zu task::wait ist es sicher, diese Funktion in einer UWP-App im ASTA -Thread (Application STA) aufzurufen.

Die when_any-Funktion

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::p air-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, when_any ist nicht blockieren und ist sicher, in einer UWP-App im ASTA-Thread aufzurufen.

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.

Zwei Möglichkeiten, dies zu erreichen, sind die Verwendung einer Fortsetzung oder das Starten einer Aufgabe und das Warten auf ein Ereignis innerhalb der Arbeitsfunktion der Aufgabe. 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 vorerkennte Aufgabe verfügen, können Sie ein Vorgangsabschlussereignis und eine spätere Verkettung dieses Abschlussereignisses mit der vorerkennten Aufgabe erstellen, wenn sie verfügbar wird. 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 Parallelität::task_completion_event Klasse trägt dazu bei, diese Zusammensetzung von Aufgaben zu vereinfachen. 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, das verwendet wird, um eine Aufgabe zu implementieren, task_completion_event die nach einer Verzögerung abgeschlossen wird, finden Sie unter How to: Create a Task that Completes After a Delay.

Aufgabengruppen

Eine Aufgabengruppe organisiert eine Sammlung von Aufgaben. 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.

Die PPL verwendet die Parallelität::task_group und parallele Klassen::structured_task_group , um Aufgabengruppen darzustellen, und die Parallelitätsklasse::task_handle Klasse, um die Aufgaben darzustellen, die in diesen Gruppen ausgeführt werden. 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 unterteilt Aufgabengruppen in diese beiden Kategorien: 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

Die PPL definiert außerdem den Parallelitätsalgorithmus::p arallel_invoke , der die structured_task_group Klasse verwendet, um eine Reihe von Aufgaben parallel auszuführen. Da der parallel_invoke-Algorithmus eine kompaktere Syntax aufweist, wird empfohlen, diesen, sofern möglich, anstelle der structured_task_group-Klasse zu verwenden. Im Thema "Parallele Algorithmen " wird ausführlicher beschrieben parallel_invoke .

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 Verzweigung und Verknüpfungsparallelitä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 zum Abbruch 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 Ausnahmebehandlungsmodell finden Sie unter "Ausnahmebehandlung".

Vergleich von task_group mit structured_task_group

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. Die Ausnahme dieser Regel ist die Parallelität::structured_task_group::cancel und parallelcurrency::structured_task_group::is_canceling-Methoden . Eine untergeordnete Aufgabe kann diese Methoden aufrufen, um die übergeordnete Aufgabengruppe abzubrechen oder das Abbrechen jederzeit zu überprüfen.

Sie können zusätzliche Aufgaben für ein task_group Objekt ausführen, nachdem Sie die Parallelität::task_group::wait oder concurrency::task_group::run_and_wait-Methode aufgerufen haben. Wenn Sie hingegen zusätzliche Aufgaben für ein structured_task_group Objekt ausführen, nachdem Sie die Parallelität::structured_task_group::wait oder concurrency::structured_task_group::run_and_wait-Methoden 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. Verwenden Sie die Parallelität::make_task-Funktion , um ein task_handle Objekt zu erstellen, wie im folgenden einfachen Beispiel gezeigt:

// 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);
}

Um Aufgabenhandles für Fälle zu verwalten, in denen Sie über eine variable Anzahl von Aufgaben verfügen, verwenden Sie eine Stapelzuordnungsroutine wie _malloca oder eine Containerklasse, z. B. std::vector.

task_group und structured_task_group unterstützen die Möglichkeit eines Abbruchs. Weitere Informationen zum Abbruch 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.

Vollständige Beispiele zur Verwendung des parallel_invoke Algorithmus finden Sie unter How to: Use parallel_invoke to Write a Parallel Sort Routine and How to: Use parallel_invoke to Execute Parallel Operations. Ein vollständiges Beispiel, in dem die task_group Klasse zum Implementieren asynchroner Futures verwendet wird, finden Sie unter Walkthrough: Implementing Futures.

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, das diese Punkte veranschaulicht, finden Sie im Abschnitt "Grundlegendes zur Abbruch- und Ausnahmebehandlung von Auswirkungen auf die Objektvernichtung " in den bewährten Methoden im Dokument "Parallel Patterns Library". Weitere Informationen zu den Abbruch- und Ausnahmebehandlungsmodellen in der PPL finden Sie unter "Abbruch und Ausnahmebehandlung".

Titel Beschreibung
Vorgehensweise: 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.
Vorgehensweise: 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.
Vorgehensweise: Erstellen einer Aufgabe, die nach einer Verzögerung abgeschlossen wird Zeigt, wie Sie mithilfe der taskKlassen cancellation_tokencancellation_token_sourceeine task_completion_event Aufgabe erstellen, 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