Windows-Apps
Inhaltsverzeichnis reduzieren
Inhaltsverzeichnis erweitern
Informationen
Das angeforderte Thema wird unten angezeigt. Es ist jedoch nicht in dieser Bibliothek vorhanden.

Aufgabenparallelität (Concurrency Runtime)

 

Veröffentlicht: März 2016

Die neueste Dokumentation zu Visual Studio 2017 finden Sie unter Dokumentation zu Visual Studio 2017.

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

Sie verwenden Aufgaben, wenn Sie asynchronen Code schreiben und ein Vorgang erst ausgeführt werden soll, nachdem der asynchrone Vorgang abgeschlossen ist. Beispielsweise können Sie eine Aufgabe asynchron aus einer Datei gelesen und dann eine andere Aufgabe – eine Fortsetzungsaufgabe, die weiter unten 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.

System_CAPS_ICON_tip.jpg Tipp

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

  • 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. Die Aufgabenklasse verwendet den Windows-ThreadPool als zugehörigen Planer und nicht die Concurrency Runtime.

  • Verwenden Sie Aufgabengruppen (die Concurrency:: task_group Klasse oder die Concurrency:: parallel_invoke Algorithmus) Wenn Sie parallele Arbeitsvorgänge in kleinere Teile zerlegen, und warten Sie, bis diese kleineren Schritte abschließen möchten.

  • Verwenden der Concurrency::task::then Methode, um Fortsetzungen zu erstellen. Ein 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.

  • Verwendung Concurrency:: HYPERLINK "http://msdn.microsoft.com/library/system.threading.tasks.task.whenall (v=VS.110).aspx" When_all zum Erstellen einer Aufgabe, die nach Abschluss jedes Mitglied einer Reihe von Aufgaben abgeschlossen ist. Verwendung Concurrency:: when_any zum Erstellen einer Aufgabe, die abgeschlossen wird, nachdem ein Mitglied einer Reihe 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.

  • Um zu erfahren, wie die Runtime Ausnahmen behandelt, die von Aufgaben und Aufgabengruppen ausgelöst werden, finden Sie unter Exception Handling.

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.

Um dieses Problem zu beheben, verwenden Sie einen intelligenten Zeiger, z. B. Std:: shared_ptr, um die Variable zu umschließen, und übergeben 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 Lambda-Ausdrücken finden Sie unter Lambda-Ausdrücke.

Sie können die Concurrency:: Task Klasse, um Aufgaben zu einem Satz abhängiger Vorgänge zu kombinieren. Dieses kompositionsmodell wird durch das Konzept der unterstützt Fortsetzungen. Eine Fortsetzung ermöglicht Code, der ausgeführt wird, wenn der vorherige oder Vorgänger, 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.

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, geben Sie einen Arbeitsfunktion die den Aufgabentext ausführt. Bei dieser Arbeitsfunktion kann es sich um eine Lambda-Funktion, einen Funktionszeiger oder ein Funktionsobjekt handeln. Rufen Sie eine Aufgabe abgeschlossen wird, ohne das Ergebnis zu warten, die Concurrency::task::wait Methode. Die task::wait -Methode gibt ein Concurrency:: task_status -Wert, der angibt, ob die Aufgabe abgeschlossen oder abgebrochen wurde. Um das Ergebnis des Vorgangs abzurufen, rufen die Concurrency Methode. 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 Verwendung der Concurrency:: create_task -Funktion können Sie die auto Schlüsselwort 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 über den Task-Mechanismus für die Ausnahmebehandlung finden Sie unter Exception Handling.

Ein Beispiel für die task, Concurrency:: task_completion_event, Abbruch, finden Sie unter Exemplarische Vorgehensweise: Herstellen einer Verbindung mithilfe von Aufgaben und XML-HTTP-Anforderungen. (Die task_completion_event-Klasse wird weiter unten in diesem Dokument beschrieben.)

System_CAPS_ICON_tip.jpg Tipp

Um Details zu erfahren, die bestimmte Aufgaben in Windows 8.x Store -apps finden Sie unter asynchrone Programmierung in C++ und Erstellen von asynchronen Vorgängen in C++ für Windows Store-Apps.

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 bereitgestellt Fortsetzungsaufgaben. Eine Fortsetzungsaufgabe (auch kurz als Fortsetzung bezeichnet) ist eine asynchrone Aufgabe, die von einer anderen Aufgabe, bekannt aufgerufen wird als die Vorgänger, wenn der Vorgänger abgeschlossen 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 8.x 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 folgende Beispiel wird das vorherige geändert, verwenden Sie die Concurrency::task::then Methode zum Planen einer Fortsetzung, die den Wert der Vorgängeraufgabe ausgibt, sobald es 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 bezeichnet asynchrone Entpacken. 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 8.x 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
*/

System_CAPS_ICON_important.jpg 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.

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 akzeptiert T bezeichnet man als einen wertbasierte Fortsetzung. Eine wertbasierte Fortsetzung wird für die Ausführung geplant, wenn die Vorgängeraufgabe ohne Fehler abgeschlossen und nicht abgebrochen wird. Eine Fortsetzung, die Typ akzeptiert task<T> als Parameter wird als bezeichnet ein aufgabenbasierte Fortsetzung. 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 vorangehende Aufgabe abgebrochen wurde, task::get löst Concurrency:: task_canceled. 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.

Dieser Abschnitt beschreibt die when_all und Concurrency:: when_any -Funktionen, die Ihnen helfen können verfassen, mehrere Vorgänge, 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 eine std::Vektor -Objekt, 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.
*/

System_CAPS_ICON_note.jpg 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.
    */

Sie können auch angeben, in diesem Beispieltask<vector<int>> 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 8.x 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>

  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.
System_CAPS_ICON_tip.jpg Tipp

when_all ist eine nicht blockierende Funktion, die task als Ergebnis erzeugt. Im Gegensatz zu Task:: wait, gefahrlos zum Aufrufen dieser Funktion einer Windows 8.x Store app auf dem ASTA-(Application STA-) Thread.

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 eine Std:: Pair -Objekt, 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.

System_CAPS_ICON_note.jpg 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

System_CAPS_ICON_tip.jpg Tipp

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

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. Jedoch, wenn Sie nicht über die Vorgängeraufgabe verfügen, können Sie erstellen ein aufgabenabschlussereignis und höher, Ereignis mit der Vorgängeraufgabe verketten, sobald dieser verfügbar ist. 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 für die task_completion_event zum Implementieren einer Aufgabe, die nach einer Verzögerung abgeschlossen wird, finden Sie unter Gewusst wie: Erstellen einer Aufgabe abgeschlossen wird nach einer Verzögerung.

Ein Aufgabengruppe verwaltet eine Auflistung 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 Concurrency:: task_group und Concurrency:: structured_task_group Klassen, Aufgabengruppen und task_handle Klasse darstellen, die Aufgaben, die in diesen Gruppen ausgeführt. 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 Aufgabengruppen in 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.

System_CAPS_ICON_important.jpg Wichtig

Die PPL definiert auch die Concurrency:: parallel_invoke -Algorithmus, der verwendet die structured_task_group Klasse einen Satz von Aufgaben parallel ausgeführt. Da der parallel_invoke-Algorithmus eine kompaktere Syntax aufweist, wird empfohlen, diesen, sofern möglich, anstelle der structured_task_group-Klasse zu verwenden. Das Thema parallele Algorithmen beschreibt parallel_invoke genauer.

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 bezeichnet Fork-Join Parallelität. 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.

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 Ausnahmebehandlung finden Sie unter Exception Handling.

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 von dieser Regel ist die Concurrency::structured_task_group::cancel und 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 ausführen, auf ein task_group -Objekt nach dem Aufruf der Concurrency:: task_group:: oder run_and_wait Methode. Umgekehrt, wenn Sie zusätzliche Aufgaben ausführen, auf ein structured_task_group -Objekt nach dem Aufruf der structured_task_group oder Concurrency::structured_task_group::run_and_wait Methoden, 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 der Concurrency:: make_task -Funktion zum Erstellen einer 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);
}

Verwenden Sie zum Verwalten von Aufgabenhandles für Fälle, in denen Sie eine Variable Anzahl von Aufgaben haben, eine Routine stapelzuordnung wie _malloca oder ein Containerklasse, z. B. std::Vektor.

task_group und structured_task_group unterstützen die Möglichkeit eines Abbruchs. Weitere Informationen über das Abbrechen finden Sie unter Abbruch.

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 für die Verwendung der parallel_invoke -Algorithmus finden Sie unter wie: mithilfe von Parallel_invoke zum parallelen sortieren Schreiben einer und wie: Parallel_invoke zum Ausführen von parallelen Vorgängen. Für ein vollständiges Beispiel, verwendet die task_group Klasse zur Implementierung asynchroner Futures finden Sie unter Exemplarische Vorgehensweise: Implementieren von Futures.

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 die verstehen wie Abbruch und Behandlung von Einfluss auf die Zerstörung Abschnitt der empfohlenen Vorgehensweisen im Dokument zur Parallel Patterns Library. Weitere Informationen zu den Abbruch und Ausnahmebehandlung Modelle in der PPL finden Sie unter Abbruch und Exception Handling.

TitelBeschreibung
Gewusst wie: Verwenden von Parallel_invoke um parallele Sortierung Schreiben einerErläutert, wie die Leistung des bitonischen Sortieralgorithmus mit dem parallel_invoke-Algorithmus verbessert werden.
Gewusst wie: Verwenden von Parallel_invoke zum Ausführen von parallelen VorgängenErlä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.Veranschaulicht, wie die task, cancellation_token_source, cancellation_token, und task_completion_event Klassen zum Erstellen einer Aufgabe, die nach einer Verzögerung abgeschlossen wird.
Exemplarische Vorgehensweise: Implementieren von FuturesZeigt, 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.

Task-Klasse (Concurrency Runtime)

Task_completion_event-Klasse

When_all-Funktion

When_any-Funktion

Task_group-Klasse

Parallel_invoke-Funktion

Structured_task_group-Klasse

Anzeigen:
© 2017 Microsoft