Funkční paralelismus (Concurrency Runtime)

V modulu Concurrency Runtime je úloha jednotkou práce, která provádí určitou úlohu a obvykle běží paralelně s jinými úkoly. Úkol lze rozdělit do dalších, jemněji odstupňovaných úkolů uspořádaných do skupiny úkolů.

Úlohy použijete při psaní asynchronního kódu a chcete, aby po dokončení asynchronní operace došlo k nějaké operaci. Můžete například použít úlohu k asynchronnímu čtení ze souboru a pak použít jiný úkol – úkol pokračování, který je vysvětlen dále v tomto dokumentu – ke zpracování dat po jejich zpřístupnění. Naopak skupiny úkolů můžete použít k dekompilování paralelní práce na menší části. Předpokládejme například, že máte rekurzivní algoritmus, který rozdělí zbývající práci do dvou oddílů. Skupiny úloh můžete použít ke souběžnému spuštění těchto oddílů a následné čekání na dokončení dělené práce.

Tip

Pokud chcete použít stejnou rutinu pro každý prvek kolekce paralelně, použijte paralelní algoritmus, například concurrency::p arallel_for, místo úlohy nebo skupiny úloh. Další informace o paralelních algoritmech naleznete v tématu Paralelní algoritmy.

Klíčové body

  • Když předáte proměnné výrazu lambda odkazem, musíte zaručit, že životnost této proměnné přetrvává, dokud se úkol nedokončí.

  • Při psaní asynchronního kódu používejte úlohy (concurrency::task class). Třída úloh používá fond vláken Systému Windows jako svůj plánovač, nikoli Concurrency Runtime.

  • Skupiny úloh ( concurrency::task_group třída nebo algoritmus concurrency::p arallel_invoke ) použijte, pokud chcete paralelní práci rozložit na menší části a pak počkejte na dokončení těchto menších částí.

  • K vytvoření pokračování použijte metodu concurrency::task::then . Pokračování je úloha, která se spouští asynchronně po dokončení jiného úkolu. K vytvoření řetězce asynchronní práce můžete připojit libovolný počet pokračování.

  • Pokračování na základě úlohy je vždy naplánováno na spuštění, když se dokončí úkol s tecedentem, i když je zrušena nebo vyvolá výjimku.

  • Použijte souběžnost::when_all k vytvoření úkolu, který se dokončí po dokončení každého člena sady úkolů. Použijte souběžnost::when_any k vytvoření úkolu, který se dokončí po dokončení jednoho člena sady úkolů.

  • Úlohy a skupiny úkolů se mohou účastnit mechanismu zrušení knihovny PPL (Parallel Patterns Library). Další informace naleznete v tématu Zrušení v PPL.

  • Informace o tom, jak modul runtime zpracovává výjimky vyvolané úlohami a skupinami úloh, najdete v tématu Zpracování výjimek.

V tomto dokumentu

Používání výrazů lambda

Z důvodu jejich stručné syntaxe jsou výrazy lambda běžným způsobem, jak definovat práci prováděnou úlohami a skupinami úloh. Tady je několik tipů k používání:

  • Vzhledem k tomu, že úlohy obvykle běží na vláknech na pozadí, mějte na paměti životnost objektu při zachycení proměnných ve výrazech lambda. Když zachytíte proměnnou podle hodnoty, vytvoří se kopie této proměnné v těle lambda. Při zachycení pomocí odkazu se kopie nevytáčí. Proto se ujistěte, že životnost jakékoli proměnné, kterou zaznamenáte pomocí odkazu, prožije úlohu, která ji používá.

  • Když předáte výraz lambda úkolu, nezachyťte proměnné, které jsou přiděleny v zásobníku odkazem.

  • Explicitní informace o proměnných, které zachytáváte ve výrazech lambda, abyste mohli zjistit, co zachytáváte podle hodnoty a odkazu. Z tohoto důvodu doporučujeme nepoužívat [=] výrazy lambda ani [&] možnosti.

Běžným vzorem je, když jeden úkol v řetězci pokračování přiřadí proměnné a jiný úkol tuto proměnnou přečte. Nelze zachytit podle hodnoty, protože každý úkol pokračování bude obsahovat jinou kopii proměnné. U proměnných přidělených zásobníkem také nelze zachytit odkazem, protože proměnná už nemusí být platná.

Chcete-li tento problém vyřešit, použijte inteligentní ukazatel, například std::shared_ptr, k zabalení proměnné a předání inteligentního ukazatele podle hodnoty. Tímto způsobem lze základní objekt přiřadit a číst z něj a prožít úkoly, které ho používají. Tuto techniku použijte, i když je proměnná ukazatelem nebo popisovačem počítaným odkazem (^) na objekt prostředí Windows Runtime. Tady je základní příklad:

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

Další informace o výrazech lambda najdete v tématu Výrazy lambda.

Třída úkolu

Pomocí třídy concurrency::task můžete vytvářet úkoly do sady závislých operací. Tento model složení je podporován konceptem pokračování. Pokračování umožňuje, aby se kód spustil při dokončení předchozího úkolu nebo úlohy s tecedentem. Výsledek úkolu s předanou jako vstup jednomu nebo více úkolům pokračování. Po dokončení úlohy s teciedentem jsou všechny úkoly pokračování, které na ni čekají, naplánovány ke spuštění. Každý úkol pokračování obdrží kopii výsledku úkolu, který je tecedent. Tyto úkoly pokračování můžou být také pro další pokračování, a tím vytvořit řetěz úkolů. Pokračování vám pomůžou vytvářet řetězy libovolných délek úkolů, které mají mezi sebou specifické závislosti. Kromě toho se úkol může účastnit zrušení buď před zahájením úkolů, nebo ve spolupráci, když je spuštěn. Další informace o tomto modelu zrušení naleznete v tématu Zrušení v PPL.

task je třída šablony. Parametr T typu je typ výsledku, který je vytvořen úlohou. Tento typ může být void v případě, že úkol nevrací hodnotu. T nelze použít const modifikátor.

Při vytváření úkolu zadáte pracovní funkci , která provádí tělo úkolu. Tato pracovní funkce je ve formě funkce lambda, ukazatele funkce nebo objektu funkce. Pokud chcete počkat na dokončení úkolu bez získání výsledku, zavolejte metodu concurrency::task::wait . Metoda task::wait vrátí hodnotu concurrency::task_status , která popisuje, jestli byl úkol dokončen nebo zrušen. Pokud chcete získat výsledek úkolu, zavolejte metodu concurrency::task::get . Tato metoda volá task::wait čekání na dokončení úlohy, a proto blokuje provádění aktuálního vlákna, dokud nebude výsledek k dispozici.

Následující příklad ukazuje, jak vytvořit úlohu, počkat na výsledek a zobrazit jeho hodnotu. Příklady v této dokumentaci používají funkce lambda, protože poskytují stručnější syntaxi. Při použití úkolů ale můžete použít také ukazatele na funkce a objekty funkcí.

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

Při použití funkce concurrency::create_task můžete místo deklarování typu použít auto klíčové slovo. Představte si například tento kód, který vytvoří a vytiskne matici identity:

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

Funkci můžete použít create_task k vytvoření ekvivalentní operace.

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

Pokud dojde k výjimce při provádění úlohy, zařadí modul runtime tuto výjimku při následném volání task::get nebo task::waitpokračování na základě úlohy. Další informace o mechanismu zpracování výjimek úloh naleznete v tématu Zpracování výjimek.

Příklad, který používá task, concurrency::task_completion_event, zrušení, viz Návod: Připojení Using Tasks and XML HTTP Requests. (Třída task_completion_event je popsána dále v tomto dokumentu.)

Tip

Podrobnosti, které jsou specifické pro úlohy v aplikacích pro UPW, najdete v tématu Asynchronní programování v jazyce C++ a vytváření asynchronních operací v jazyce C++ pro aplikace pro UPW.

Úkoly pokračování

V asynchronním programování je u jedné asynchronní operace při dokončení velmi běžné vyvolat druhou operaci a předat do ní data. Tradičně se to provádí pomocí metod zpětného volání. V modulu Concurrency Runtime je stejná funkce poskytována úlohami pokračování. Úloha pokračování (označovaná také jako pokračování) je asynchronní úkol, který je vyvolán jiným úkolem, který se označuje jako tecedent, když se dokončí tecedent. Pomocí pokračování můžete:

  • Předejte data z objektu antecedent do pokračování.

  • Zadejte přesné podmínky, za kterých je pokračování vyvoláno nebo není vyvoláno.

  • Zrušte pokračování buď před tím, než začne, nebo kooperativně, když běží.

  • Uveďte rady o tom, jak se má pokračování naplánovat. (To platí jenom pro aplikace Univerzální platforma Windows (UPW). Další informace najdete v tématu Vytváření asynchronních operací v jazyce C++ pro aplikace pro UPW.)

  • Vyvolá více pokračování ze stejného objektu antecedent.

  • Vyvolá jedno pokračování, pokud jsou dokončeny všechny nebo některé z více tecedentů.

  • Řetěz pokračování jeden po druhém na libovolnou délku.

  • Použití pokračování ke zpracování výjimek, které vyvolá antecedent.

Tyto funkce umožňují spouštět jeden nebo více úkolů po dokončení prvního úkolu. Můžete například vytvořit pokračování, které komprimuje soubor po prvním úkolu, který ho přečte z disku.

Následující příklad upraví předchozí metodu tak, aby používala souběžnost::task::pak metodu k naplánování pokračování, které vytiskne hodnotu úlohy stecedent, pokud je k dispozici.

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

Úkoly můžete zřetězovat a vnořit na libovolnou délku. Úkol může mít také více pokračování. Následující příklad znázorňuje základní řetězec pokračování, který třikrát zvýší hodnotu předchozího úkolu.

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

Pokračování může také vrátit další úkol. Pokud nedojde ke zrušení, provede se tato úloha před následným pokračováním. Tato technika se označuje jako asynchronní rozbalení. Asynchronní rozbalení je užitečné, když chcete provést další práci na pozadí, ale nechcete, aby aktuální úkol blokoval aktuální vlákno. (To je běžné v aplikacích pro UPW, kde se pokračování můžou spouštět ve vlákně uživatelského rozhraní). Následující příklad ukazuje tři úkoly. První úkol vrátí jiný úkol, který je spuštěn před pokračováním úkolu.

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

Důležité

Pokud pokračování úkolu vrátí vnořený úkol typu N, výsledný úkol má typ N, ne task<N>a dokončí se po dokončení vnořeného úkolu. Jinými slovy pokračování provede rozbalení vnořené úlohy.

Pokračování založená na hodnotách a úlohách

Při zadání objektu, jehož návratový task typ je T, můžete zadat hodnotu typu T nebo task<T> jeho pokračování úkolů. Pokračování, které přebírá typT, se označuje jako pokračování založené na hodnotách. Pokračování založené na hodnotách je naplánováno na spuštění, když se dokončí úloha typu antecedent bez chyby a není zrušena. Pokračování, které přebírá typ task<T> jako jeho parametr, se označuje jako pokračování založené na úlohách. Pokračování na základě úlohy je vždy naplánováno na spuštění, když se dokončí úkol s tecedentem, i když je zrušena nebo vyvolá výjimku. Potom můžete volat task::get , abyste získali výsledek úkolu s tecedentem. Pokud byla zrušena úloha s tecedentem, task::get vyvolá souběžnost::task_canceled. Pokud úloha typu antecedent vyvolala výjimku, task::get znovu tuto výjimku načte. Pokračování na základě úkolu není při zrušení úkolu označeno jako zrušeno.

Psaní úkolů

Tato část popisuje funkce souběžnosti::when_all a souběžnost::when_any , které vám můžou pomoct při vytváření více úloh pro implementaci běžných vzorů.

Funkce when_all

Funkce when_all vytvoří úkol, který se dokončí po dokončení sady úkolů. Tato funkce vrátí objekt std::vector , který obsahuje výsledek každého úkolu v sadě. Následující základní příklad používá when_all k vytvoření úkolu, který představuje dokončení tří dalších úkolů.

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

Poznámka

Úkoly, na when_all které přejdete, musí být jednotné. Jinými slovy, musí všechny vrátit stejný typ.

Pomocí syntaxe můžete také && vytvořit úkol, který se dokončí po dokončení sady úkolů, jak je znázorněno v následujícím příkladu.

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

Pokračování je běžné použít společně s when_all provedením akce po dokončení sady úkolů. Následující příklad upraví předchozí, aby se vytiskl součet tří úkolů, které každý z nich vytvoří int výsledek.

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

V tomto příkladu můžete také určit task<vector<int>> , aby se vytvořilo pokračování založené na úkolu.

Pokud je některý úkol v sadě úkolů zrušen nebo vyvolá výjimku, when_all okamžitě se dokončí a nečeká na dokončení zbývajících úkolů. Pokud dojde k vyvolání výjimky, modul runtime znovu vyvolá výjimku při volání task::get nebo task::wait na objekt úkolu, který when_all vrací. Pokud vyvolá více než jeden úkol, modul runtime vybere jeden z nich. Proto se ujistěte, že po dokončení všech úkolů budete sledovat všechny výjimky; Neošetřená výjimka úlohy způsobí ukončení aplikace.

Tady je funkce nástroje, kterou můžete použít k zajištění toho, aby váš program sledoval všechny výjimky. Pro každou úlohu v zadaném rozsahu aktivuje všechny výjimky, observe_all_exceptions ke kterým došlo, že došlo k opakování, a pak ji spolkne.

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

Představte si aplikaci pro UPW, která používá C++ a XAML a zapisuje sadu souborů na disk. Následující příklad ukazuje, jak používat when_all a observe_all_exceptions zajistit, aby program sledoval všechny výjimky.

// 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();
        }
    });
}
Spuštění tohoto příkladu
  1. V souboru MainPage.xaml přidejte Button ovládací prvek.
<Button x:Name="Button1" Click="Button_Click">Write files</Button>
  1. V MainPage.xaml.h přidejte tyto předávací deklarace do private oddílu MainPage deklarace třídy.
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. V souboru MainPage.xaml.cpp implementujte obslužnou rutinu Button_Click události.
// 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. V souboru MainPage.xaml.cpp implementujte WriteFilesAsync , jak je znázorněno v příkladu.

Tip

when_all je neblokující funkce, která vygeneruje task výsledek. Na rozdíl od úkolu::wait je bezpečné tuto funkci volat v aplikaci UPW ve vlákně ASTA (Application STA).

Funkce when_any

Funkce when_any vytvoří úkol, který se dokončí při dokončení prvního úkolu v sadě úkolů. Tato funkce vrátí objekt std::p air , který obsahuje výsledek dokončeného úkolu a index úkolu v sadě.

Funkce when_any je užitečná zejména v následujících scénářích:

  • Nadbytečné operace. Zvažte algoritmus nebo operaci, které lze provést mnoha způsoby. Pomocí funkce můžete when_any vybrat operaci, která se dokončí jako první, a pak zrušit zbývající operace.

  • Prokládané operace. Můžete spustit několik operací, které musí být dokončeny, a pomocí when_any funkce zpracovat výsledky při dokončení každé operace. Po dokončení jedné operace můžete spustit jednu nebo více dalších úloh.

  • Omezené operace. Pomocí funkce můžete when_any rozšířit předchozí scénář omezením počtu souběžných operací.

  • Operace, jejichž platnost vypršela. Pomocí funkce můžete when_any vybrat jeden nebo více úkolů a úkol, který se dokončí po určitém čase.

Stejně jako u when_all, je běžné použít pokračování, které musí when_any provést akci při prvním v sadě úkolů dokončení. Následující základní příklad používá when_any k vytvoření úkolu, který se dokončí při dokončení první ze tří dalších úkolů.

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

V tomto příkladu můžete také určit task<pair<int, size_t>> , aby se vytvořilo pokračování založené na úkolu.

Poznámka

Stejně jako u when_all, úkoly, které when_any předáváte, musí vrátit stejný typ.

Pomocí syntaxe můžete také || vytvořit úkol, který se dokončí po prvním úkolu v sadě dokončených úkolů, jak je znázorněno v následujícím příkladu.

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

Tip

Stejně jako v when_allpřípadě , when_any je neblokující a je bezpečné volat v aplikaci UPW ve vlákně ASTA.

Zpožděné spuštění úlohy

Někdy je nutné zpozdit provádění úkolu, dokud není splněna podmínka, nebo spustit úkol v reakci na externí událost. Například v asynchronním programování může být nutné spustit úlohu v reakci na událost dokončení vstupně-výstupních operací.

Dvěma způsoby, jak toho dosáhnout, jsou použití pokračování nebo spuštění úkolu a čekání na událost uvnitř pracovní funkce úkolu. Existují však případy, kdy není možné použít některou z těchto technik. Chcete-li například vytvořit pokračování, musíte mít úkol s určitou architekturou. Pokud však nemáte úkol s objektem antecedent, můžete vytvořit událost dokončení úkolu a později zřetězení této události dokončení na úkol s tecedent, jakmile bude k dispozici. Kromě toho, protože čekající úloha také blokuje vlákno, můžete události dokončení úkolů použít k provádění práce při dokončení asynchronní operace a tím uvolnit vlákno.

Souběžnost ::task_completion_event třída pomáhá zjednodušit takové složení úkolů. task Stejně jako třída je typ parametru T typu výsledek, který je vytvořen úlohou. Tento typ může být void v případě, že úkol nevrací hodnotu. T nelze použít const modifikátor. Objekt task_completion_event je obvykle poskytován vláknu nebo úkolu, který bude signalizovat, když je hodnota pro ni k dispozici. Současně je jedna nebo více úkolů nastaveno jako naslouchací procesy této události. Když je událost nastavená, úkoly naslouchacího procesu se dokončí a jejich pokračování se naplánují tak, aby se spustily.

Příklad, který používá task_completion_event k implementaci úkolu, který se dokončí po zpoždění, naleznete v tématu Postupy: Vytvoření úkolu, který se dokončí po zpoždění.

Skupiny úloh

Skupina úkolů organizuje kolekci úkolů. Skupiny úloh odsílaly úkoly do fronty krádeže práce. Plánovač odebere úlohy z této fronty a spustí je na dostupných výpočetních prostředcích. Po přidání úkolů do skupiny úkolů můžete počkat na dokončení všech úkolů nebo zrušení úkolů, které ještě nebyly spuštěny.

PPL používá třídy concurrency::task_group a concurrency::structured_task_group k reprezentaci skupin úloh a třídy concurrency::task_handle představují úlohy, které běží v těchto skupinách. Třída task_handle zapouzdřuje kód, který provádí práci. Stejně jako třída task přichází pracovní funkce ve formě funkce lambda, ukazatele funkce nebo objektu funkce. Obvykle nemusíte pracovat s task_handle objekty přímo. Místo toho předáte pracovní funkce skupině úloh a skupina úloh vytvoří a spravuje task_handle objekty.

PPL rozdělí skupiny úkolů do těchto dvou kategorií: nestrukturované skupiny úkolů a strukturované skupiny úkolů. PPL používá task_group třídu k reprezentaci nestrukturovaných skupin úloh a structured_task_group třídy k reprezentaci strukturovaných skupin úkolů.

Důležité

PPL také definuje concurrency::p arallel_invoke algoritmus, který používá structured_task_group třídu ke spuštění sady úkolů paralelně. parallel_invoke Vzhledem k tomu, že algoritmus má stručnější syntaxi, doporučujeme, abyste ji místo structured_task_group třídy použili, pokud je to možné. Téma Paralelní algoritmyparallel_invoke podrobněji popisuje.

Použijte parallel_invoke , když máte několik nezávislých úkolů, které chcete spustit současně, a před pokračováním musíte počkat na dokončení všech úkolů. Tato technika se často označuje jako fork a spojení paralelismu. Použijte task_group , když máte několik nezávislých úkolů, které chcete spustit najednou, ale chcete počkat, až se úkoly dokončí později. Můžete například přidat úkoly do objektu task_group a počkat na dokončení úkolů v jiné funkci nebo z jiného vlákna.

Skupiny úloh podporují koncept zrušení. Zrušení umožňuje signalizovat všechny aktivní úkoly, které chcete zrušit celkovou operaci. Zrušení také zabraňuje spuštění úkolů, které ještě nebyly spuštěny. Další informace o zrušení naleznete v tématu Zrušení v PPL.

Modul runtime také poskytuje model zpracování výjimek, který umožňuje vyvolat výjimku z úlohy a zpracovat tuto výjimku při čekání na dokončení přidružené skupiny úloh. Další informace o tomto modelu zpracovánívýjimekch

Porovnání task_group s structured_task_group

Přestože doporučujeme použít task_group třídu nebo parallel_invoke místo structured_task_group ní, existují případy, kdy chcete použít structured_task_group, například při psaní paralelního algoritmu, který provádí proměnlivý počet úkolů nebo vyžaduje podporu pro zrušení. Tato část vysvětluje rozdíly mezi třídami task_group a structured_task_group třídami.

Třída task_group je bezpečná pro přístup z více vláken. Proto můžete do objektu task_group přidat úlohy z více vláken a počkat nebo zrušit task_group objekt z více vláken. Konstrukce a zničení objektu structured_task_group musí nastat ve stejném lexikálním rozsahu. Kromě toho musí všechny operace s objektem structured_task_group nastat ve stejném vlákně. Výjimkou tohoto pravidla je souběžnost::structured_task_group::cancel a concurrency::structured_task_group::is_canceling metody. Podřízený úkol může těmto metodám volat zrušení nadřazené skupiny úloh nebo kdykoli zkontrolovat zrušení.

Po volání metody concurrency:::task_group::wait nebo concurrency::task_group::run_and_wait můžete na objektu spustit další úlohytask_group. Pokud na objektu spustíte další úlohy structured_task_group po volání souběžnosti::structured_task_group::wait nebo concurrency::structured_task_group::run_and_wait metod, chování není definováno.

Vzhledem k tomu, že structured_task_group třída se nesynchronizuje mezi vlákny, má menší režii provádění než task_group třída. Proto pokud váš problém nevyžaduje plánování práce z více vláken a nemůžete použít parallel_invoke algoritmus, structured_task_group může vám třída pomoct psát lépe výkonnější kód.

Pokud použijete jeden structured_task_group objekt uvnitř jiného structured_task_group objektu, musí být vnitřní objekt dokončen a zničen před dokončením vnějšího objektu. Třída task_group nevyžaduje dokončení vnořených skupin úloh před dokončením vnější skupiny.

Nestrukturované skupiny úkolů a strukturované skupiny úkolů pracují s popisovači úkolů různými způsoby. Pracovní funkce můžete předat přímo do objektu task_group . task_group Objekt za vás vytvoří a bude spravovat popisovač úkolu. Třída structured_task_group vyžaduje správu objektu task_handle pro každý úkol. Každý task_handle objekt musí zůstat platný po celou dobu životnosti přidruženého structured_task_group objektu. Pomocí funkce concurrency::make_task vytvořte task_handle objekt, jak je znázorněno v následujícím základním příkladu:

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

Ke správě úloh pro případy, kdy máte proměnlivý počet úkolů, použijte rutinu přidělování zásobníku, jako je _malloca nebo třída kontejneru, například std::vector.

Zrušení a task_groupstructured_task_group podpora. Další informace o zrušení naleznete v tématu Zrušení v PPL.

Příklad

Následující základní příklad ukazuje, jak pracovat se skupinami úloh. Tento příklad používá algoritmus parallel_invoke k souběžnému provádění dvou úloh. Každý úkol přidá dílčí úkoly do objektu task_group . Všimněte si, že task_group třída umožňuje, aby do ní souběžně přidalo více úkolů.

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

Následuje ukázkový výstup pro tento příklad:

Message from task: Hello
Message from task: 3.14
Message from task: 42

parallel_invoke Vzhledem k tomu, že algoritmus spouští úlohy souběžně, může se pořadí výstupních zpráv lišit.

Kompletní příklady, které ukazují, jak používat parallel_invoke algoritmus, najdete v tématu Postupy: Použití parallel_invoke k zápisu rutiny paralelního řazení a postupy: Použití parallel_invoke ke spouštění paralelních operací. Úplný příklad, který používá task_group třídu k implementaci asynchronních futures, naleznete v části Návod: Implementace futures.

Robustní programování

Ujistěte se, že rozumíte roli zpracování zrušení a výjimek při používání úloh, skupin úloh a paralelních algoritmů. Například ve stromu paralelní práce úloha, která je zrušena, zabraňuje spuštění podřízených úkolů. To může způsobit problémy v případě, že jedna z podřízených úloh provádí operaci, která je pro vaši aplikaci důležitá, například uvolnění prostředku. Kromě toho, pokud podřízený úkol vyvolá výjimku, může se tato výjimka rozšířit prostřednictvím destruktoru objektu a způsobit nedefinované chování v aplikaci. Příklad, který ukazuje tyto body, najdete v části Principy zpracování zrušení a zpracování výjimek v části Osvědčené postupy v dokumentu Knihovny paralelních vzorů. Další informace o modelech zpracování zrušení a výjimek v PPL naleznete v tématu Zpracování zrušení a výjimek.

Titulek Popis
Postupy: Použití algoritmu parallel_invoke k zápisu rutiny paralelního třídění Ukazuje, jak použít parallel_invoke algoritmus ke zlepšení výkonu algoritmu řazení bitových hodnot.
Postupy: Použití algoritmu parallel_invoke k provádění paralelních operací Ukazuje, jak použít parallel_invoke algoritmus ke zlepšení výkonu programu, který provádí více operací se sdíleným zdrojem dat.
Postupy: Vytvoření úlohy, která se dokončí po prodlevě Ukazuje, jak pomocí task, cancellation_token_source, cancellation_tokena task_completion_event třídy vytvořit úkol, který se dokončí po zpoždění.
Návod: Implementace tříd future Ukazuje, jak zkombinovat existující funkce v modulu Concurrency Runtime do něčeho, co dělá víc.
Knihovna PPL (Parallel Patterns Library) Popisuje PPL, který poskytuje imperativní programovací model pro vývoj souběžných aplikací.

Reference

task – třída (Concurrency Runtime)

task_completion_event – třída

when_all – funkce

when_any – funkce

task_group – třída

parallel_invoke – funkce

structured_task_group – třída