Udostępnij za pośrednictwem


Równoległość zadania (współbieżność środowiska wykonawczego)

W środowisku uruchomieniowym współbieżności zadanie jest jednostką pracy, która wykonuje określone zlecenie i zazwyczaj działa równolegle z innymi zadaniami.Zadanie może być rozłożone na zadania dodatkowe z bardziej szczegółowymi zasadami, które są zorganizowane w grupy zadań.

Używasz zadań do pisania kodu asynchronicznego i chcesz, aby niektóre operacje wystąpiły po zakończeniu operacji asynchronicznej.Na przykład można użyć zadania do asynchronicznego odczytania z pliku, a następnie użyć innego zadania — zadania kontynuacji, co jest opisane w dalszej części tego dokumentu — do przetwarzania danych, gdy staną się dostępne.I odwrotnie, można użyć grup zadań do rozkładu pracy równolegle na mniejsze części.Na przykład załóżmy, że masz algorytm cykliczny, która dzieli pozostałą pracę na dwie partycje.Grupy zadań umożliwiają jednoczesne uruchamianie tych partycji, a następnie czekanie na zakończenie pracy podzielonej.

PoradaPorada

Jeśli dla każdego elementu kolekcji równolegle chcesz stosować taką samą procedurę, użyj algorytmu równoległego, takiego jak concurrency::parallel_for, a nie zadania lub grupy zadań.Aby uzyskać więcej informacji dotyczących algorytmów równoległych, zobacz Algorytmy równoległe.

Kwestie kluczowe

  • Podczas przekazywania zmiennych do wyrażenia lambda przez odniesienie, musisz zagwarantować, że okres istnienia tej zmiennej utrzymuje się do zakończenia zadania.

  • Użyj zadań (klasa concurrency::task) podczas wpisywania kodu asynchronicznego.

  • Użyj grup zadaniowych (klasa concurrency::task_group lub algorytm concurrency::parallel_invoke) kiedy chcesz rozkładać równoległą pracę na mniejsze kawałki i zaczekaj, aż te mniejsze kawałki zakończą.

  • Użyj metody concurrency::task::then do tworzenia kontynuacji.Kontynuacja jest zadaniem uruchamianym asynchronicznie po zakończeniu innego zadania.Możesz połączyć dowolną liczbę kontynuacji w postaci łańcucha zadań asynchronicznych.

  • Kontynuacja oparta na zadaniach zawsze jest zaplanowana do uruchomienia po zakończeniu zadania poprzedzającego, wtedy, gdy zadanie poprzedzające jest anulowane lub zgłasza wyjątek.

  • Użyj concurrency::when_all, aby utworzyć zadanie, które kończy się po tym, jak każdy członek zestawu zadań zakończy pracę.Użyj concurrency::when_any, aby utworzyć zadanie, które kończy się po tym, jak jeden z członków zestawu zadań zakończy pracę.

  • Zadania i grup zadań mogą uczestniczyć w mechanizmie anulowania Biblioteki wzorców równoległych (PPL).Aby uzyskać więcej informacji, zobacz Anulowanie w PPL.

  • Aby dowiedzieć się, jak środowisko wykonawcze obsługuje wyjątki wyrzucane przez grupy zadań i zadania, zobacz Obsługa wyjątków we współbieżności środowiska wykonawczego.

W tym dokumencie

  • Korzystając z wyrażenia Lambda

  • Klasa zadania

  • Zadania ciągłości

  • Kontynuacje na bazie wartości w porównaniu do kontynuacji związanych z zadaniami

  • Tworzenie zadań

    • Funkcja when_all

    • Funkcja when_any

  • Wykonanie opóźnionego zadania

  • Grupy zadań

  • Porównywanie task_group z structured_task_group

  • Przykład

  • Niezawodne programowanie

Korzystając z wyrażenia Lambda

Ze względu na ich zwięzłą składnię wyrażenia lambda są typowym sposobem definiowania pracy, która jest wykonywana przez zadania i grupy zadań.Oto kilka porad o użyciu:

  • Ponieważ zadania zazwyczaj są uruchamiane na wątkach w tle, należy pamiętać o okresie istnienia obiektu podczas przechwytywania zmiennych w wyrażeniach lambda.W przypadku przechwytywania zmiennej według wartości, kopia tej zmiennej powstaje w treści lambda.W przypadku przechwytywania według odniesienia, kopia nie jest tworzona.W związku z tym upewnij się, że okres istnienia dowolnej zmiennej przechwyconej przez odniesienie przeżywa zadanie, które go używa.

  • Jeśli przekazujesz wyrażenie lambda do zadania, nie przechwytuj zmiennych, które są przydzielane do stosu przez odwołanie.

  • Należy ujawniać przechwytywane zmienne w wyrażeniach lambda, tak aby można było zidentyfikować, co udało się przechwycić według wartości, a co przez odniesienie.Z tego powodu firma Microsoft zaleca, aby nie używać opcji [=] ani [&] dla wyrażeń lambda.

Wspólny wzorzec polega na tym, że jedno zadanie w łańcuchu kontynuacji jest przypisywane do zmiennej, a inne zadanie odczytuje tę zmienną.Nie można przechwycić według wartości, ponieważ każde zadanie kontynuacji będzie przechowywało inną kopię zmiennej.Dla zmiennych przydzielanych ze stosów również nie można przechwycić według odwołań, ponieważ zmienna może nie być już prawidłowa.

Aby rozwiązać ten problem, użyj inteligentnego wskaźnika, takiego jak std::shared_ptr, aby zawinąć zmienną i przekazać inteligentny wskaźnik przez wartość.W ten sposób podstawowy obiekt można przypisać i odczytywać i będzie on nakreślał zadania, które go używają.Użyj tej techniki, nawet wtedy, gdy zmienna jest wskaźnikiem lub zliczanym uchwytem (^) do obiektu Windows Runtime.Poniżej przedstawiono prosty przykład:

// 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
*/

Aby uzyskać więcej informacji na temat wyrażeń lambda, zobacz Wyrażenia lambda w języku C++.

[U góry]

Klasa zadania

Możesz użyć klasy concurrency::task do redagowania zadań w zestaw operacji zależnych.Ten model kompozycji jest obsługiwany przez kontynuacje.Kontynuacja umożliwia wykonanie kodu po zakończeniu poprzedniego zadania poprzedzającego.Wynik zadania poprzedzającego jest przekazywany jako dane wejściowe do jednego lub kilku zadań kontynuacji.Po zakończeniu pracy przez zadanie poprzedzające, wszelkie zadania kontynuacji, które na nie czekają są zaplanowane do wykonania.Każde zadanie utrzymania otrzymuje kopię wyników zadania poprzedzającego.Z kolei te zadania kontynuacji mogą być również zadaniami poprzedzającymi dla innych kontynuacji, tworząc w ten sposób łańcuch zadań.Kontynuacji ułatwiają tworzenie łańcuchów dowolnej długości zadań, które mają szczególne współzależności między nimi.Ponadto zadanie może uczestniczyć w anulowaniu albo przed uruchomieniem zadania albo w sposób współpracujący, gdy jest uruchomione.Aby uzyskać więcej informacji dotyczących metody anulowania, zobacz Anulowanie w PPL.

task jest klasą szablonu.Parametr typu T to typu wyniku, który jest wytwarzany przez zadanie.Ten typ może być void, jeśli zadanie nie zwraca wartości.T nie może używać modyfikatora const.

Tworząc zadanie, zapewniasz funkcję roboczą, która wykonuje treść zadania.Ta funkcja pracy ma postać funkcji lambda, wskaźnika funkcji lub obiektu funkcji.Aby czekać na zakończenie zadania bez uzyskania wyniku, wywołaj metodę concurrency::task::wait.Metoda task::wait zwraca wartość concurrency::task_status, która opisuje, czy zadanie zostało zakończone lub anulowane.Aby uzyskać wynik zadania, wywołaj metodę concurrency::task::get.Ta metoda wywołuje task::wait, aby czekała na zakończenie zadania, a tym samym blokuje realizację bieżącego wątku do chwili udostępnienia wyniku.

Poniższy przykład ukazuje, jak utworzyć zadanie, czekać na jego wynik i wyświetlić jego wartość.Przykłady w niniejszej dokumentacji używają funkcji lambda, ponieważ zapewniają one bardziej zwięzłą składnię.Jednakże można także użyć wskaźników funkcji i obiektów funkcji podczas korzystania z zadań.

// 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
*/

Kiedy używasz funkcji concurrency::create_task, możesz użyć słowa kluczowego auto zamiast deklarowania typu.Na przykład rozważmy ten kod, który tworzy i drukuje macierz tożsamości:

// 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
*/

Możesz użyć funkcji create_task, aby utworzyć operację równoważną.

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

Jeśli wyjątek jest zgłaszany podczas wykonywania zadania, środowisko uruchomieniowe kieruje ten wyjątek w następnym wywołaniu do metody task::get lub task::wait lub do kontynuacji związanej z zadaniami.Aby uzyskać więcej informacji na mechanizmu obsługi wyjątków w zadaniach, zobacz Obsługa wyjątków we współbieżności środowiska wykonawczego.

Aby uzyskać przykład, która korzysta z obiektu task, concurrency::task_completion_event lub anulowania, zobacz Wskazówki: Łączenie za pomocą zadań i żądań XML HTTP. (Klasa task_completion_event jest opisana w dalszej części tego dokumentu).

PoradaPorada

Aby uzyskać szczegółowe informacje, które są specyficzne dla zadań w aplikacjach Windows Store aplikacji, zobacz Asynchronous programming in C++ i Tworzenie operacji asynchronicznych w języku C++ dla aplikacji sklepu Windows Store.

[U góry]

Zadania ciągłości

W programowaniu asynchronicznych jest częste, że jedna operacja asynchroniczna po zakończeniu wywołuje drugą operację i przekazuje dane do niej.Tradycyjnie odbywa się to przy użyciu metod wywołania zwrotnego.W środowisku uruchomieniowym współbieżności taką samą funkcjonalność świadczą zadania kontynuacji.Zadanie kontynuacji (znane również jako kontynuacja) to asynchroniczne zadanie, które jest wywoływane przez inne zadanie, znane jako poprzedzające, po jego zakończeniu.Korzystając z kontynuacji, można:

  • Przekazywanie danych od zadania poprzedzającego do kontynuacji.

  • Określ dokładne warunki, na jakich kontynuacja jest lub nie jest wywoływana.

  • Anuluj kontynuację albo przed jej rozpoczęciem albo w trakcie jej działania.

  • Dostarczanie wskazówek na temat, jak kontynuacja powinna zostać zaplanowana. (Dotyczy to tylko aplikacji Windows Store.Aby uzyskać więcej informacji, zobacz Tworzenie operacji asynchronicznych w języku C++ dla aplikacji sklepu Windows Store.)

  • Wywoływanie wielu kontynuacji z tego samego zadania poprzedzającego.

  • Wywołanie jednej kontynuacji po ukończeniu wszystkich lub dowolnego z wielu zadań poprzedzających.

  • Kontynuacje łańcucha kolejno do każdej dowolnej długości.

  • Użyj kontynuacji, aby obsłużyć wyjątki wyrzucane przez element poprzedzający.

Funkcje te pozwalają na wykonywanie jedno lub kilku zadań po zakończeniu pierwszego zadania.Na przykład można utworzyć kontynuację, która kompresuje plik, gdy pierwsze zadanie odczyta go z dysku.

Poniższy przykład modyfikuje poprzedni, aby użyć metody concurrency::task::then, aby zaplanować kontynuację, która drukuje wartość zadania poprzedzającego, gdy jest ono dostępne.

// 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
*/

Możesz połączyć i zagnieździć zadania do dowolnej długości.Zadanie może też mieć wiele kontynuacji.Poniższy przykład ilustruje podstawową kontynuację łańcucha, który trzykrotnie zwiększa wartość poprzedniego zadania.

// 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
*/

Kontynuacja może również zwracać inne zadanie.Jeśli nie ma żadnego anulowania, to zadanie jest wykonywane przed kolejną kontynuacją.Ta technika jest znana jako asynchronicznego rozpakowywanie.Rozpakowanie asynchroniczne jest przydatne, gdy potrzeba wykonania dodatkowej pracy w tle, ale bieżące zadanie nie chce zablokować bieżącego wątku. (Jest to często w aplikacjach Windows Store aplikacje, gdzie kontynuacje można uruchomić na wątku interfejsu użytkownika).Poniższy przykład ukazuje trzy zadania.Pierwsze zadanie zwraca inne zadanie, które jest uruchamiane przed kontynuacją zadania.

// 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
*/
Ważna uwagaWażne

Kiedy kontynuacja zadania zwraca zagnieżdżone zadanie typu N, wynikowe zadanie ma typ N, nie task<N>i kończy po zakończeniu zadania zagnieżdżonego.Innymi słowy kontynuacja wykonuje rozpakowanie zagnieżdżonego zadania.

[U góry]

Kontynuacje na bazie wartości w porównaniu do kontynuacji związanych z zadaniami

Mając obiekt task, którego typem zwracanym jest T, można dostarczyć wartości typu T lub task<T> do jego zadań kontynuacji.Kontynuacja, która ma typ T, jest znana jako kontynuacja oparta na wartościach.Kontynuacja oparta na wartościach jest zaplanowana do wykonania, gdy poprzedzające zadanie zakończy się bez błędów i nie zostało anulowane.Kontynuacja, która ma typ task<T> i parametr, jest znana jako kontynuacja oparta na zadaniu.Kontynuacja oparta na zadaniach zawsze jest zaplanowana do uruchomienia po zakończeniu zadania poprzedzającego, wtedy, gdy zadanie poprzedzające jest anulowane lub zgłasza wyjątek.Następnie możesz wywołać task::get , aby uzyskać wynik zadania poprzedzającego.Jeśli zadanie poprzedzające zostało anulowane, metoda task::get zgłasza wyjątek concurrency::task_canceled.Jeśli zadanie poprzedzające zgłosiło wyjątek, metoda task::get ponownie zgłasza ten wyjątek.Kontynuacja oparta na zadaniach nie jest oznaczona jako anulowana po anulowaniu zadania poprzedzającego.

[U góry]

Tworzenie zadań

Ten rozdział opisuje funkcje concurrency::when_all i concurrency::when_any, które mogą pomóc w komponowaniu wielu zadań do wdrażania typowych wzorców.

Funkcja when_all

Funkcja when_all tworzy zadanie, które kończy się po wykonaniu zestawu zadań.Ta funkcja zwraca obiekt std::vector, który zawiera wynik każdego zadania w zestawie.Poniższy przykład podstawowy używa when_all do utworzenia zadania, które reprezentuje ukończenie trzech innych zadań.

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

[!UWAGA]

Zadania przekazywane do when_all muszą być jednolite.Innymi słowy muszą one wszystkie zwracać ten sam typ.

Możesz również użyć składni && do tworzenia zadania, które kończy się po wykonaniu zestawu zadań, jak pokazano w następującym przykładzie.

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

Bardzo często używa się kontynuacji wraz z obiektem when_all do wykonania akcji po zakończeniu zestawu zadań.Poniższy przykład modyfikuje poprzedni, aby drukować sumę trzech zadań, z których każde produkuje wynik int.

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

W tym przykładzie można również określić parametr task<vector<int>> do wygenerowania kontynuacji opartej na zadaniach.

Jeśli dowolne zadanie w zestawie zadań zostanie anulowana lub zgłasza wyjątek, funkcja when_all natychmiast kończy działanie i nie czeka na zakończenie pozostałych zadań.Jeśli jest zgłaszany wyjątek, środowisko uruchomieniowe ponownie zgłasza ten wyjątek podczas wywoływania metody task::get lub task::wait dla obiektu zadania, który zwraca funkcja when_all.Jeśli więcej niż jedno zadanie zgłasza, środowisko uruchomieniowe wybiera jeden z nich.W związku z tym upewnij się, że przestrzegasz wszystkich wyjątków, po zakończeniu wszystkich zadań; nieobsłużony wyjątek zadania powoduje zakończenie działania aplikacji.

Oto jest funkcja narzędziowa, której można użyć w celu zapewnienia, że program odczytuje wszystkie wyjątki.Dla każdego zadania z podanego zakresu obiekt observe_all_exceptions ponownie wyzwala każdy wyjątek, który wystąpił, a następnie go zapisuje.

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

Należy wziąć pod uwagę aplikację Windows Store, która korzysta z języka C++ i XAML i zapisuje zbiór plików na dysku.Poniższy przykład ukazuje, jak używać when_all i observe_all_exceptions do zapewnienia, że program rejestruje wszystkie wyjątki.

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

Aby uruchomić ten przykład

  1. W pliku MainPage.xaml dodaj formant Button.

    <Button x:Name="Button1" Click="Button_Click">Write files</Button>
    
  2. W pliku MainPage.xaml.h należy dodać te deklaracje przechodzenia do przodu do sekcji private deklaracji klasy MainPage.

    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. W pliku MainPage.xaml.cpp zaimplementuj program obsługi zdarzeń Button_Click.

    // 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. W pliku MainPage.xaml.cpp zaimplementuj funkcję WriteFilesAsync, jak pokazano w przykładzie.

PoradaPorada

when_all jest funkcją bez blokowania, która wytwarza task jako wynik.W przeciwieństwie do task::wait, można bezpiecznie wywołać tę funkcję w aplikacji Windows Store w wątku ASTA (Aplikacja STA).

[U góry]

Funkcja when_any

Funkcja when_any tworzy zadanie, które kończy się po zakończeniu pierwszego zadania w zestawie zadań.Ta funkcja zwraca obiekt std::pair, który zawiera wynik zakończonego zadania i indeks tego zadania w zestawie.

Funkcja when_any jest szczególnie użyteczna w następujących scenariuszach:

  • Operacje nadmiarowe.Należy wziąć pod uwagę algorytm lub operację, które mogą być wykonywane na wiele sposobów.Możesz użyć funkcji when_any, aby wybrać operację, która zakończy się pierwsza, a następnie anulować pozostałe operacje.

  • Operacje z przeplotem.Możesz uruchomić wiele operacji, które muszą się zakończyć i używać funkcji when_any do przetwarzania wyników, po zakończeniu operacji.Po zakończeniu jednej operacji można uruchomić jedno lub więcej dodatkowych zadań.

  • Operacje ograniczane.Możesz użyć funkcji when_any, aby rozszerzyć poprzedni scenariusz poprzez ograniczenie liczby operacji jednoczesnych.

  • Wygasłe operacje.Możesz użyć funkcji when_any, aby wybrać jedno lub więcej zadań, oraz zadanie, który kończy się po określonym czasie.

Jak z when_all, bardzo często używa się kontynuacji wraz z obiektem when_any do wykonania akcji po zakończeniu pierwszego zestawu zadań.Poniższy przykład podstawowy używa when_any do utworzenia zadania, które zostaje ukończone, gdy pierwsze z trzech innych zadań zostaje ukończone.

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

W tym przykładzie można również określić parametr task<pair<int, size_t>> do wygenerowania kontynuacji opartej na zadaniach.

[!UWAGA]

Podobnie jak w przypadku when_all, wszystkie zadania, które są przekazywane do when_any, muszą zwracać taki sam typ.

Możesz również użyć składni || do tworzenia zadania, które kończy się po zakończeniu pierwszego zadania w zestawie, jak pokazano w następującym przykładzie.

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

PoradaPorada

Podobnie jak w przypadku when_all, when_any jest bez blokowania i jest bezpieczny do wywołania aplikacji Windows Store w wątku ASTA.

[U góry]

Wykonanie opóźnionego zadania

Czasami konieczne jest opóźnienie wykonania zadania, dopóki nie zostanie spełniony jakiś warunek, lub aby uruchomić zadanie w odpowiedzi na zdarzenie zewnętrzne.Na przykład w asynchronicznym programowaniu może trzeba będzie uruchomić zadanie w odpowiedzi na zdarzenie zakończenia operacji we/wy.

Dwa sposoby na osiągnięcie tego celu to kontynuacja lub uruchomienie zadania i oczekiwanie na to, że zadziała zdarzenie wewnątrz funkcji pracy.Jednakże istnieją przypadki, gdy nie jest możliwe korzystanie z jednej z tych technik.Na przykład aby utworzyć kontynuację, należy mieć zadanie poprzedzające.Jednakże jeśli nie masz zadania poprzedzającego, można utworzyć zdarzenie zakończenia zadania i później połączyć to zdarzenie zakończenia z zadaniem poprzedzającym, kiedy stanie się dostępne.Ponadto ponieważ zadanie oczekujące blokuje również wątek, można użyć zdarzeń zakończenia zadania do wykonywania pracy po zakończeniu operacji asynchronicznej, a w efekcie zwolnić wątek.

Klasa Concurrency::task_completion_event pomaga uprościć taki skład zadań.Podobnie jak klasa task, parametr typu T jest typem wyniku generowanym przez zadanie.Ten typ może być void, jeśli zadanie nie zwraca wartości.T nie może używać modyfikatora const.Zazwyczaj obiekt task_completion_event jest zapewniany dla wątku lub zadania, które wyda sygnał, gdy wartość dla niego stanie się dostępna.W tym samym czasie co najmniej jedno zadanie jest ustawiane jako detektory zdarzenia.Gdy zdarzenie jest ustawione, kończą się zadania odbiornika a ich kontynuacje są planowane do uruchomienia.

Aby uzyskać przykład, która korzysta z obiektu task_completion_event do zaimplementowania zadania, które zakończy się z opóźnieniem, zobacz Porady: tworzenie zadania kończonego po opóźnieniu.

[U góry]

Grupy zadań

Grupa zadań organizuje zbiory zadań.Grupy zadań wypychają zadania do kolejki przechwytywania pracy.Harmonogram usuwa zadania z tej kolejki i wykonuje je na dostępnych zasobach komputerowych.Po dodaniu zadań do grupy zadań można poczekać na zakończenie wszystkich zadań i anulowania zadania, które nie zostały rozpoczęte.

PPL korzysta z klas concurrency::task_group i concurrency::structured_task_group do reprezentowania grup zadań oraz z klasy concurrency::task_handle do reprezentowania zadań uruchamianych w tych grupach.Klasa task_handle hermetyzuje kod, który wykonuje pracę.Podobnie jak klasa task, ta funkcja pracy ma postać funkcji lambda, wskaźnika funkcji lub obiektu funkcji.Zazwyczaj nie trzeba pracować z obiektami task_handle bezpośrednio.Zamiast tego funkcje robocze są przekazywane do grupy zadań, a grupa zadań tworzy i zarządza obiektami task_handle.

PPL dzieli grupy zadań na dwie kategorie: grupy zadań bez struktury i grupy wykonujący zadania strukturalne.PPL korzysta z klasy task_group do reprezentowania grup zadań bez struktury oraz z klasy structured_task_group do reprezentowania grup wykonujących zadania strukturalne.

Ważna uwagaWażne

PPL definiuje również algorytm concurrency::parallel_invoke, który korzysta z klasy structured_task_group, do równoległego wykonywania zestawu zadań.Ponieważ algorytm parallel_invoke ma bardziej zwięzłą składnię, zaleca się używać go zamiast klasy structured_task_group, gdy tylko to możliwe.Temat Algorytmy równoległe opisuje parallel_invoke bardziej szczegółowo.

Użyj parallel_invoke kiedy masz kilka niezależnych zadań, które mają zostać zrealizowane w tym samym czasie i musisz poczekać na zakończenie pracy przez wszystkie zadania, zanim będziesz mógł kontynuować.Technika ta jest często określana jako równoległość fork and join.Użyj task_group kiedy masz kilka niezależnych zadań, które mają zostać zrealizowane w tym samym czasie, lecz chcesz poczekać na zakończenie pracy przez wszystkie zadania, zanim będziesz mógł kontynuować.Na przykład można dodać zadania do obiektu task_group i poczekać na ich zakończenie w funkcji lub z innego wątku.

Grupy zadań obsługują koncepcję anulowania.Anulowanie umożliwia zasygnalizowanie wszystkim aktywnym zadaniom, że chcesz anulować operację ogólną.Anulowanie zapobiega także zadaniom, których uruchamianie jeszcze się nie rozpoczęło.Aby uzyskać więcej informacji dotyczących anulowania, zobacz Anulowanie w PPL.

Środowisko wykonawcze zawiera również model obsługi wyjątków, który umożliwia zgłoszenie wyjątku z zadania i obsługę tego wyjątku podczas oczekiwania na ukończenie działania skojarzonej grupy zadań.Aby uzyskać więcej informacji na temat tego modelu obsługi wyjątków, zobacz Obsługa wyjątków we współbieżności środowiska wykonawczego.

[U góry]

Porównywanie task_group z structured_task_group

Chociaż firma Microsoft zaleca użycie task_group lub parallel_invoke zamiast klasy structured_task_group, są przypadki, gdzie chcesz użyć structured_task_group, na przykład podczas wpisywania algorytmu równoległego, który wykonuje liczbę zmiennych zadań lub wymaga wsparcia dla odwołania.Ten rozdział wyjaśnia różnice między klasami task_group i structured_task_group.

Klasa task_group jest bezpieczna dla wątków.Dlatego można dodawać zadania do obiektu task_group z wielu wątków i czekać lub anulować obiekt task_group z wielu wątków.Budowa i niszczenie obiektu structured_task_group musi występować w tym samym zakresie leksykalnym.Dodatkowo wszystkie operacje na obiekcie structured_task_group musi przypadać w tym samym wątku.Wyjątkiem od tej reguły są metody concurrency::structured_task_group::cancel i concurrency::structured_task_group::is_canceling.Zadanie podrzędne może wywoływać te metody, aby anulować grupę nadrzędną zadań lub sprawdzić anulowanie w dowolnym momencie.

Możesz przeprowadzić dodatkowe zadania na obiekcie task_group po wywołaniu metody concurrency::task_group::wait lub concurrency::task_group::run_and_wait.I odwrotnie, po uruchomieniu zadań dodatkowych na obiekcie structured_task_group po wywołaniu metody concurrency::structured_task_group::wait lub concurrency::structured_task_group::run_and_wait zachowanie jest niezdefiniowane.

Ponieważ klasa structured_task_group nie synchronizuje między wątkami, ma ona mniejsze obciążenie niż klasa task_group.Dlatego, jeśli Twój problem nie wymaga planowania pracy z wielu wątków i nie możesz użyć algorytmu parallel_invoke, klasa structured_task_group może Ci pomóc w pisaniu lepiej zachowującego się kodu.

Jeśli użyty zostanie jeden obiekt structured_task_group wewnątrz innego obiektu structured_task_group, obiekt wewnętrzny zakończyć działanie i zostać zniszczone przed zakończeniem działania obiektu zewnętrznego.Klasa task_group nie wymaga od zagnieżdżonych grup zadań ukończenia prac przed zakończeniem prac przez grupę zewnętrzną.

Grupy zadań bez struktury i zadania strukturalnych pracują z programami do obsługi zadań na różne sposoby.Możesz przekazać funkcje pracy bezpośrednio do obiektu task_group; obiekt task_group będzie tworzył i zarządzał programami do obsługi zadań dla Ciebie.Klasa structured_task_group wymaga od użytkownika zarządzania obiektem task_handle dla każdego zadania.Każdy obiekt task_handle musi zachować ważność przez cały okres istnienia związanego z nim obiektu structured_task_group.Użyj funkcji concurrency::make_task do utworzenia obiektu task_handle, jak pokazano w poniższym przykładzie:

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

Aby zarządzać programami do obsługi zadań dla przypadków o zmienną liczbę zadań, użyj procedury alokacji stosu, takiej jak _malloca lub klasy kontenera, takiej jak std::vector.

Zarówno task_group, jak i structured_task_group obsługuje anulowanie.Aby uzyskać więcej informacji dotyczących anulowania, zobacz Anulowanie w PPL.

[U góry]

Przykład

Poniższy przykład podstawowy pokazuje, jak pracować z grupami zadań.Ten przykład używa algorytmu parallel_invoke do wykonywania dwóch zadań jednocześnie.Każde zadanie dodaje podzadania obiektu task_group.Należy zauważyć, że klasa task_group umożliwia dodanie jednocześnie wielu zadań.

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

Oto próbka danych wyjściowych dla tego przykładu.

  

Ponieważ algorytm parallel_invoke uruchamia zadania jednocześnie, kolejność komunikatów wyjściowych może być inna.

Aby uzyskać kompletny przykład przedstawiający metody korzystania z algorytmu parallel_invoke, zobacz Porady: używanie parallel_invoke do napisania procedury sortowania równoległego i Porady: korzystanie z parallel_invoke do przeprowadzania operacji równoległych.Aby uzyskać pełny przykład używający klasy task_group, aby zaimplementować funkcje asynchroniczne, zobacz Wskazówki: wdrażanie przyszłych operacji.

[U góry]

Niezawodne programowanie

Upewnij się, że rozumiesz rolę anulowania i obsługi wyjątków, kiedy korzystasz z zadań, grup zadań i algorytmów równoległe.Na przykład w drzewie równoległej pracy zadanie, które zostało anulowane, zapobiega uruchamianiu zadań podrzędnych.Może to powodować problemy, jeśli jedno z zadań podrzędnych wykonuje operację, która jest ważna dla aplikacji, taką jak zwalnianie zasobu.Ponadto jeśli zadanie podrzędne zgłasza wyjątek, ten wyjątek może się propagować przez destruktora obiektu i spowodować niezdefiniowane zachowanie w aplikacji.Aby uzyskać przykład, który ilustruje te punkty, zobacz sekcję Zrozum w jaki sposób unieważnienie i obsługa wyjątków wpływają na zniszczenie obiektu w rozdziale o najlepszych praktykach w dokumencie Biblioteka równoległych wzorców.Aby uzyskać więcej informacji o modelach anulowania i obsługi wyjątków w bibliotece PPL, zobacz Anulowanie w PPL i Obsługa wyjątków we współbieżności środowiska wykonawczego.

[U góry]

Tematy pokrewne

Tytuł

Opis

Porady: używanie parallel_invoke do napisania procedury sortowania równoległego

Pokazuje, jak użyć algorytm parallel_invoke, aby zwiększyć wydajność algorytmu sortowania bitonicznego.

Porady: korzystanie z parallel_invoke do przeprowadzania operacji równoległych

Pokazuje, jak użyć algorytm parallel_invoke, aby zwiększyć wydajność programu, który wykonuje wiele operacji na udostępnionym źródle danych.

Porady: tworzenie zadania kończonego po opóźnieniu

Pokazuje, jak użyć klas task, cancellation_token_source, cancellation_token, i task_completion_event, aby utworzyć zadanie, które zakończy się z opóźnieniem.

Wskazówki: wdrażanie przyszłych operacji

Pokazuje, jak połączyć istniejącą funkcje we współbieżnym środowisku wykonawczym w coś, co ma większe możliwości.

Biblioteka równoległych wzorców (PLL)

Opisano PPL, która przewiduje imperatywny model programowania dla aplikacji współbieżnych.

Odwołanie

task — Klasa (współbieżność środowiska wykonawczego)

task_completion_event — Klasa

when_all — Funkcja

when_any — Funkcja

task_group — Klasa

parallel_invoke — Funkcja

structured_task_group — Klasa