Sdílet prostřednictvím


Funkční paralelismus (Concurrency Runtime)

V modulu runtime souběžnosti je úkol jednotka práce, která provádí určitou úlohu a obvykle běží souběžně s jinými úkoly.Úkol lze rozložit na další detailnější úkoly, které jsou uspořádány do skupiny úloh.

Úkoly se používají při zápisu asynchronního kódu a vyžadují, aby k některým operacím došlo po dokončení asynchronní operace.Můžete například použít úkol pro asynchronní čtení ze souboru a potom použít jiný úkol, úkol pokračování, jak je vysvětleno dále v tomto dokumentu – zpracování dat po jejich zpřístupnění.Naopak můžete použít úkoly skupiny k rozložení paralelní práce na menší kousky.Předpokládejme například, že máte rekurzivní algoritmus, který slouží k rozdělení zbývající práce na dva oddíly.Skupiny úloh můžete použít k souběžnému spouštění těchto oddílů a pak čekat na dokončení rozdělené práce.

Tip

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

Klíčové body

  • Při předání proměnných do výrazu lambda pomocí odkazu musíte zaručit, že životnost proměnné potrvá, dokud neskončí úloha.

  • Použijte úkoly (třída concurrency::task) při psaní asynchronního kódu.

  • Použijte skupiny úloh (třída concurrency::task_group nebo algoritmus concurrency::parallel_invoke), když chcete rozložit paralelní práci na menší části, a poté vyčkejte na dokončení těchto menších částí.

  • Použijte metodu concurrency::task::then k vytvoření pokračování.Pokračování je úkol, který běží asynchronně po dokončení jiného úkolu.Můžete připojit libovolný počet pokračování k vytvoření řetězce asynchronní práce.

  • Vždy je naplánováno provedení pokračování založené na úkolu, když je předchůdce dokončen, i když je předchozí úloha zrušena nebo vyvolá výjimku.

  • Použijte concurrency::when_all k vytvoření úlohy, která se dokončí po dokončení každého člena sady úloh.Použijte concurrency::when_any k vytvoření úlohy, která se dokončí po dokončení jednoho člena sady úloh.

  • Úkoly a skupiny úkolů se mohou účastnit mechanismu zrušení knihovny PPL.Další informace naleznete v tématu Zrušení v knihovně PPL.

  • Pokud chcete z jistit, jak modul runtime zpracovává výjimky, které jsou vyvolány pomocí úloh a skupin úloh, naleznete v tématu Zpracování výjimek v Concurrency Runtime.

V tomto dokumentu

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

  • Třídy task

  • Pokračující úlohy

  • Pokračování na základě hodnoty nebo na základě úlohy

  • Skládání úloh

    • Funkce when_all

    • Funkce when_any

  • Zpožděné provedení úlohy

  • Skupiny úloh

  • Porovnání tříd task_group a structured_task_group

  • Příklad

  • Robustní programování

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

Z důvodu jejich stručné syntaxe jsou lambda výrazy běžným způsobem definování práce, která je prováděna podle úloh a skupin úloh.Zde jsou některé tipy:

  • Vzhledem k tomu, že úkoly obvykle běží na pozadí podprocesů, je dobré mít na paměti dobu životnosti objektu při zachycení proměnné ve výrazech lambda.Při zachycení proměnné podle hodnoty je vytvořena její kopie v těle lambda.Při zachycení odkazem není vytvořena kopie.Zajistěte proto, aby životnost libovolné proměnné, kterou zachytíte na základě reference, přežila úlohu, která ji používá.

  • Při předání lambda výrazu do úkolu nezachycujte proměnné, které jsou přiděleny v zásobníku dle odkazu.

  • Buďte konkrétní o proměnných, které zachytíte v lambda výrazech, aby mohli identifikovat, co právě zachycujete podle hodnoty oproti odkazům.Z tohoto důvodu doporučujeme nepoužívat možnosti [=] nebo [&] pro výrazy lambda.

Obvyklým vzorem je, když jeden úkol v řetězci pokračování přiřadí proměnné, a jiný úkol danou proměnnou přečte.Podle hodnoty nelze zachytit, protože každý úkol pokračování, by uchovával jinou kopii proměnné.Pro přidělení proměnných zásobníku také nelze zachytit odkaz, protože proměnné již nemusí být platné.

Chcete-li tento problém vyřešit, použijte inteligentní ukazatel, například std::shared_ptr, ke sbalení proměnné a přesunutí inteligentního ukazatele podle hodnoty.Tímto způsobem lze základní objekt přiřadit a číst z něj a bude bude možné jej používat nezávisle na úkolech, které jej používají.Tento postup použijte i v případě, že proměnná je ukazatel nebo referenčně započítaný popisovač pro objekt (^) běhu systému Windows.Zde 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 naleznete v tématu Výrazy lambda v jazyce C++.

[Nahoře]

Třídy task

Pomocí třídy concurrency::task můžete sestavit úlohy do sady závislých operací.Tento model sestavení je podporován pojmem pokračování.Pokračování umožňuje spuštění kódu po dokončení předchozího úkolu neboli předchůdce.Výsledek předchozí úlohy je předán jako vstup pro jeden nebo více dalších úkolů.Po dokončení předchozí úlohy jakékoli pokračující úkoly, které na ni čekají, jsou naplánovány k provedení.Každý úkol pokračování obdrží kopii výsledku předchozí úlohy.Tyto úkoly pokračování zase mohou být předchozími úkoly pro další pokračování, čímž se vytváří řetěz úkolů.Pokračování vám pomůže vytvořit úkoly libovolné délky řetězů, které mají specifické vzájemné závislosti.Kromě toho se může úkol zúčastnit ve zrušení, buď před zahájením úkolu nebo na způsob spolupráce za běhu.Další informace o tomto modelu zrušení naleznete v tématu Zrušení v knihovně PPL.

task je třída šablony.Parametr typu T je typ výsledku, který vytvořil úkol.Tento typ může být void, pokud úloha nevrací hodnotu.T neumožňuje použít modifikátor const.

Při vytváření úkolu zadáte pracovní funkci, která provede tělo úkolu.Podobně jako u třídy je tato pracovní funkce dodávána ve formě funkce lambda, ukazatele na funkci nebo objektu funkce.Chcete-li čekat na dokončení úkolu dokončit bez získání výsledku, volejte metodu concurrency::task::wait.Metoda task::wait vrátí hodnotu concurrency::task_status, která popisuje, zda úloha byla dokončena nebo zrušena.Pokud chcete získat výsledek úkolu, volejte metodu concurrency::task::get.Tato metoda volá task::wait pro čekání na dokončení úlohy, a blokuje tak provedení aktuálního vlákna, dokud nebude k dispozici výsledek.

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

// 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 použít klíčové slovo auto místo deklarování typu.Zvažte například tento kód, který vytvoří a vytiskne jednotkovou matici:

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

Pomocí funkce create_task lze vytvořit ekvivalentní operaci:

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 je vyvolána výjimka při provádění úkolu, modul runtime zařazuje tuto výjimku při následném volání task::get nebo task::wait nebo na pokračování podle úloh.Další informace o tomto mechanizmu zpracování výjimky úkolu naleznete v tématu Zpracování výjimek v Concurrency Runtime.

Příklad, který používá zrušení task, concurrency::task_completion_event, naleznete v tématu Návod: Připojení pomocí úloh a žádostí XML HTTP. (Třída task_completion_event je popsána dále v tomto dokumentu.)

Tip

Chcete-li získat podrobnosti, které jsou specifické pro úlohy v aplikacích Windows Store, uvádí téma Asynchronous programming in C++ a Vytváření asynchronních operací v jazyce C++ pro aplikace pro web Windows Store.

[Nahoře]

Pokračující úlohy

V asynchronním programování je velmi běžné, že jedna asynchronní operace při dokončení vyvolá druhou operaci a předá jí data.Tradičně bylo toto prováděno pomocí metod zpětného volání.V modulu runtime souběžnosti je stejná funkčnost zajištěna pomocí pokračujících úloh.Pokračující úloha (nebo také pouze pokračování) je asynchronní úloha, které je vyvolána jinou úlohou (nazývanou též předchůdce) jakmile je předchůdce dokončen.Pomocí pokračování můžete:

  • Předat data z předchůdce do pokračování.

  • Určete přesné podmínky, za kterých bude či nebude pokračování vyvoláno.

  • Zrušte pokračování před spuštěním nebo kooperativně za běhu.

  • Poskytnout nápovědu, jak má být pokračování plánováno. (To platí pouze pro aplikace Windows Store.Další informace naleznete v tématu Vytváření asynchronních operací v jazyce C++ pro aplikace pro web Windows Store.

  • Vyvolat ze stejného předchůdce více pokračování.

  • Vyvolat jedno pokračování, až skončí všichni nebo jen někteří z více předchůdců.

  • Zřetězit pokračování jedno po druhém do jakékoli libovolné délky.

  • Použijte pokračování pro zpracování výjimek vyvolaných předchůdcem.

Tyto funkce vám umožňují provést jednu nebo více úloh po dokončení první úlohy.Můžete například vytvořit pokračování, které komprimuje soubor po jeho prvním načtení z disku.

Následující příklad upravuje předchozí tak, aby používat metodu concurrency::task::then pro plánování pokračování, které vytiskne hodnotu předchozí úlohy, jakmile 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
*/

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

// 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 jiný úkol.Pokud neexistuje žádné zrušení, tak je tento úkol proveden před dalším pokračováním.Tato technika se nazývá asynchronní rozvinutí.Asynchronní rozbalení je užitečné, pokud chcete provést další práce na pozadí, ale nechcete, aby aktuální úkol blokoval aktuální vlákno. (To je obvyklé v aplikacích Windows Store, kde lze spustit pokračování ve vlákně uživatelského rozhraní).Následující příklad ukazuje tři úkoly.První úkol vrátí jiný úkol, který se spouští před pokračujícím úkolem.

// 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á poznámkaDůležité

Když pokračování úlohy vrátí vnořenou úlohu typu N, výsledný úkol má typ N, nikoli task<N> a skončí po dokončení vnořené úlohy.Jinými slovy pokračování provádí rozbalení vnořené úlohy.

[Nahoře]

Pokračování na základě hodnoty nebo na základě úlohy

Daný objekt task, jehož návratový typ je T, můžete zadat hodnotu typu T nebo task<T> k jeho pokračujícím úlohám.Pokračování, které převezme typ T, 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 vykonání, když je předchozí úloha dokončena bez chyb a není zrušena.Pokračování, které převezme typ task<T> jako svůj parametr, se označuje jako pokračování založené na úkolech.Vždy je naplánováno provedení pokračování založené na úkolu, když je předchůdce dokončen, i když je předchozí úloha zrušena nebo vyvolá výjimku.Potom můžete zavolat task::get k získání výsledku předchozí úlohy.Pokud předchozí úloha byla zrušena, task::get vyvolá concurrency::task_canceled.V případě že předchozí úloha vyvolala výjimku task::get znovu vyvolá tuto výjimku.Pokračování podle úloh není označeno jako zrušené, když je zrušena jeho předchozí úloha.

[Nahoře]

Skládání úloh

Tato část popisuje funkce concurrency::when_all a concurrency::when_any, které vám pomohou vytvořit více úkolů pro implementaci běžných vzorů.

Funkce when_all

Funkce when_all vytvoří úlohu, která skončí po dokončení sady úloh.Tato funkce vrací objekt std::vector, který obsahuje výsledek každého úkolu v sadě.V následujícím základním příkladu je when_all použito k vytvoření úkolu, který představuje dokončení tři 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, které se předávají do when_all, musí být jednotné.Jinými slovy musí všichni vrátit stejný typ.

Syntaxi && můžete použít také k vytvoření úlohy, která se dokončí po dokončení sady úloh, jak je znázorněno v následujícím příkladu.

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

Je běžné používat pokračování společně s when_all k provedení akce po dokončení sady úkolů.Následující příklad upravuje předchozí tisk součtu tři úkolů, ze kterých každý vytváří výsledek 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.
*/

V tomto příkladu můžete také zadat task<vector<int>> k výrobě pokračování podle úloh.

Pokud libovolný úkol v sérii úkolů je zrušen nebo vyvolá výjimku, when_all se ihned dokončí a nečeká na dokončení zbývajících úkolů.Pokud dojde k výjimce, modul runtime znovu vyvolá výjimku při volání task::get nebo task::wait na objektu úkolu, který vrátí when_all.Pokud vyvolá více než jeden úkol, modul runtime vybere jeden z nich.Proto zajistěte sledování všech výjimek po dokončení všech úloh; nezpracovaná výjimka úlohy způsobí ukončení aplikace.

Zde je nástroj pro funkci, která slouží k zajištění, že váš program dodržuje všechny výjimky.Pro každý úkol v zadané oblasti spustí observe_all_exceptions jakoukoli výjimku, která znovu vyvolala a poté pohltila danou výjimku.

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

Zvažte aplikaci Windows Store, která používá C++ a XAML a zapíše sadu souborů na disk.Následující příklad ukazuje, jak použít when_all a observe_all_exceptions k zajištění toho, že program bude sledovat 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. Do MainPage.xaml přidejte ovládací prvek Button.

    <Button x:Name="Button1" Click="Button_Click">Write files</Button>
    
  2. V MainPage.xaml.h přidejte tyto deklarace dopředu, do části private deklarace třídy 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. V souboru MainPage.xaml.cpp implementujte obslužnou rutinu události 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. Ve MainPage.xaml.cpp implementujte WriteFilesAsync, jak je uvedeno v příkladu.

Tip

when_all je neblokující funkce, která vytváří task jako výsledek.Na rozdíl od úlohy task::wait je bezpečné volat tuto funkci v aplikaci Windows Store vlákna ASTA (Application STA).

[Nahoře]

Funkce when_any

Funkce when_any vytvoří úlohu, která skončí po dokončení první úlohy v sadě úloh.Tato funkce vrací objekt std::pair, který obsahuje výsledek dokončené úlohy a index této úlohy v sadě.

Funkce when_any je zvláště užitečná v následujících situacích:

  • Nadbytečné operace.Zvažte algoritmus nebo operaci, které lze provést mnoha způsoby.Funkci when_any lze použít pro výběr operace, která skončí jako první, a pak pro zrušení zbývajících operací.

  • Prokládané operace.Můžete spustit více operací, které se všechny musí dokončit, a použít funkci when_any ke zpracování výsledků při dokončení každé operace.Po dokončení jedné operace můžete spustit jeden nebo více dalších úkolů.

  • Omezené operace.Funkci when_any lze použít k rozšíření předchozího scénáře tím, že omezíte počet souběžných operací.

  • Operace, jejichž platnost vypršela.Funkci when_any lze použít k výběru mezi jednou nebo více úlohami a úlohou, která skončí po určité době.

Stejně jako v případě when_all je běžné použití pokračování, které má when_any k provedení akce při dokončení první sady úkolů.V následujícím základním příkladu je when_any použito k vytvoření úkolu, který se dokončí spolu s dokončením prvního ze tři 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é zadat task<pair<int, size_t>> k výrobě pokračování podle úloh.

[!POZNÁMKA]

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

Syntaxi || můžete použít také k vytvoření úlohy, která se dokončí po dokončení první úlohy v sadě úloh, jak je znázorněno v následujícím příkladu.

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

Tip

Stejně jako u when_all, when_any je neblokující a je bezpečné ho volat v aplikaci Windows Store ve vlákně ASTA.

[Nahoře]

Zpožděné provedení úlohy

Občas je nutné zpoždění spuštění úlohy, dokud není splněna podmínka, nebo spuštění úlohy jako odpověď na vnější události.Například v asynchronním programování, bude pravděpodobně nutné spustit úkol jako odpověď na událost dokončení I/O.

Existují dva způsoby, jak toho dosáhnout: použití pokračování nebo spuštění úlohy a čekání na událost uvnitř pracovní funkce této úlohy.Existují však případy, kdy není možné použít některý z následujících postupů.Například chcete-li vytvořit pokračování, musíte mít předchozí úlohu.Nicméně pokud nemáte předchozí úlohu, můžete vytvořit událost dokončení úkolu a později tuto událost dokončení následujícího úkolu zřetězit, jakmile bude úkol k dispozici.Navíc vzhledem k tomu, že úkol čekání také blokuje vlákno, můžete použít události dokončení k provádění práce, když asynchronní operace skončí a tím uvolnit vlákno.

Třída concurrency::task_completion_event pomáhá zjednodušit složení těchto úkolů.Stejně jako u třídy task je parametr typu T typ výsledku, který vytvořil úkol.Tento typ může být void, pokud úloha nevrací hodnotu.T neumožňuje použít modifikátor const.Obvykle je objekt task_completion_event poskytnut do vlákna nebo úlohy, které mu dají signál, jakmile bude hodnota k dispozici.Ve stejnou dobu jeden nebo více úkolů je nastaveno jako posluchači pro tuto událost.Při nastavení události skončí úkoly modulu listener a jejich pokračování je naplánováno na spuštění.

Příklad použití task_completion_event k implementaci úkolu, který je dokončí po prodlevě, naleznete v tématu Postupy: Vytvoření úlohy, která je dokončena po prodlevě.

[Nahoře]

Skupiny úloh

Skupina úloh uspořádá kolekci úkolů.Skupiny úloh odešlou úlohy do fronty přebírající práci.Plánovač odstraní úkoly z této fronty a provede je v dostupných výpočetních prostředcích.Jakmile přidáte úlohy do skupiny úloh, můžete počkat na dokončení všech úloh nebo zrušit úlohy, které ještě nebyly spuštěny.

PPL používá třídy concurrency::task_group a concurrency::structured_task_group pro reprezentaci skupin úkolů a třídu concurrency::task_handle pro reprezentaci úlohy spuštěné v těchto skupinách.Třída task_handle zapouzdřuje kód, který provádí práci.Podobně jako u třídy task je pracovní funkce dodávána ve formě funkce lambda, ukazatele na funkci nebo objektu funkce.Obvykle není nutné pracovat s objekty task_handle přímo.Místo toho předáte pracovní funkce skupině úkolů a skupina úloh vytváří a spravuje objekty task_handle.

PPL dělí skupiny úkolů na tyto dvě kategorie: nestrukturované skupiny úkolů a strukturované skupiny úkolů.PPL používá třídu task_group pro reprezentaci nestrukturované skupiny úkolů a structured_task_group pro reprezentaci strukturované skupiny úkolů.

Důležitá poznámkaDůležité

PPL také definuje algoritmus concurrency::parallel_invoke, který používá třídu structured_task_group k současnému spuštění sady úkolů.Protože algoritmus parallel_invoke má stručnější syntaxi, doporučujeme použít místo něj třídu structured_task_group, pokud to bude možné.Téma Paralelní algoritmy podrobněji popisuje parallel_invoke.

Použijte parallel_invoke, pokud máte několik nezávislých úloh, které chcete provést současně a je nutné počkat na dokončení všech úkolů, než budete pokračovat.Tato technika je často označována jako paralelismus rozvětvení a spojení.Použijte task_group, pokud máte několik nezávislých úloh, které chcete provést současně, ale chcete počkat na dokončení úloh později.Můžete například přidat úkoly objektu task_group a počkat na dokončení funkce v jiné funkci nebo v jiném vlákně.

Skupiny úloh podporují koncept zrušení.Zrušení vám umožňuje signalizovat všem aktivním úkolům, kterým chcete zrušit celkovou operaci.Zrušení zabrání také dosud nezahájeným úkolům před spuštěním.Další informace o rušení viz Zrušení v knihovně PPL.

Modul runtime poskytuje také model pro zpracování výjimek, který umožňuje vytvořit výjimku z úkolu a zpracovat tuto výjimku při čekání na dokončení přidružené skupiny úkolů.Další informace o tomto modelu zpracování výjimky naleznete v tématu Zpracování výjimek v Concurrency Runtime.

[Nahoře]

Porovnání tříd task_group a structured_task_group

Přestože doporučujeme používat třídu task_group nebo parallel_invoke místo třídy structured_task_group, existují případy, kdy chcete použít třídu structured_task_group, například při psaní paralelního algoritmu, který provádí proměnný počet úkolů nebo vyžaduje podporu pro zrušení.V této části jsou vysvětleny rozdíly mezi třídou task_group a structured_task_group.

Třída task_group má zabezpečené vlákno.Proto můžete přidávat úkoly k task_group objektu z více vláken a počkat nebo zrušit objekt task_group z více vláken.Výstavba a likvidace objektu structured_task_group musí proběhnout ve stejném lexikálním oboru.Navíc všechny operace na objektu structured_task_group se musí vyskytovat na stejném vlákně.Výjimkou z tohoto pravidla jsou metody concurrency::structured_task_group::cancel a concurrency::structured_task_group::is_canceling.Podřízená úloha může tyto metody volat a zrušit skupinu nadřazeného úkolu nebo zrušení kdykoli zkontrolovat.

Další úkoly můžete spustit v objektu task_group po volání metody concurrency::task_group::wait nebo concurrency::task_group::run_and_wait.Naopak pokud spustíte další úkoly v objektu structured_task_group po zavolání metody concurrency::structured_task_group::wait nebo concurrency::structured_task_group::run_and_wait, pak chování není definováno.

Protože třída structured_task_group není synchronizována přes více vláken, má nižší nároky na spuštění než třída task_group.Proto pokud váš problém nevyžaduje plánování práce z více vláken a nelze použít algoritmus parallel_invoke, může vám třída structured_task_group pomoci psát lépe uskutečnitelný kód.

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

Nestrukturované skupiny úloh a strukturované skupiny úloh pracují s popisovači úloh různými způsoby.Pracovní funkce můžete předat přímo do objektu task_group; objekt task_group vytvoří a bude spravovat popisovač úkolu za vás.Třída structured_task_group vyžaduje správu objektu task_handle pro každou úlohu.Každý objekt task_handle musí zůstat platný po celou dobu trvání jeho přidruženého objektu structured_task_group.Použijte funkci concurrency::make_task k vytvoření objektu task_handle, jak je znázorněno v následujícím jednoduché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);
}

Chcete-li spravovat rutiny úloh pro případy, které mají variabilní počet úloh, použijte rutinu přidělení zásobníku, jako je _malloca nebo třída kontejneru jako například std::vector.

Obě task_group i structured_task_group podporují zrušení.Další informace o rušení viz Zrušení v knihovně PPL.

[Nahoře]

Příklad

Následující základní příklad ukazuje, jak pracovat se skupinami úloh.V tomto příkladu se používá algoritmus parallel_invoke k provedení dvou úloh současně.Každý úkol přidá dílčí úkoly do objektu task_group.Všimněte si, že třída task_group umožňuje více úkolům současně přidat do sebe úkoly.

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

  

Vzhledem k tomu, že algoritmus parallel_invoke spustí úlohy současně, může měnit pořadí výstupu zpráv.

Kompletní příklady, které ukazují, jak používat algoritmus parallel_invoke naleznete v tématech Postupy: Použití algoritmu parallel_invoke k zápisu rutiny paralelního třídění a Postupy: Použití algoritmu parallel_invoke k provádění paralelních operací.Kompletní příklad, který používá třídu task_group pro implementaci asynchronních funkcí, naleznete v tématu Návod: Implementace tříd future.

[Nahoře]

Robustní programování

Ujistěte se, že chápete role zrušení a zpracování výjimek při použití úkolů, úkolů skupiny a paralelních algoritmů.Například ve stromové struktuře paralelní práce, úkol, který je zrušen, zabraňuje spuštění podřízených úloh.To může způsobit potíže, pokud některá z podřízených úloh provádí operaci, která je důležitá pro vaši aplikaci, jako je například uvolnění prostředku.Navíc pokud podřízená úloha vyvolá výjimku, tato výjimka by se šířila destruktorem objektu a způsobila nedefinované chování ve vaší aplikaci.Příklad, který znázorňuje tyto body, naleznete v části Pochopení, jak zrušení a spuštění zpracování ovlivní odstraňování objektů v tématu Doporučené postupy v paralelních vzorcích knihovny dokumentů.Další informace o zrušení a modelech zpracování výjimek v PPL naleznete v tématu Zrušení v knihovně PPL a Zpracování výjimek v Concurrency Runtime.

[Nahoře]

Příbuzná témata

Title

Description

Postupy: Použití algoritmu parallel_invoke k zápisu rutiny paralelního třídění

Ukazuje, jak použít algoritmus parallel_invoke pro zlepšení výkonu bitonického algoritmu řazení.

Postupy: Použití algoritmu parallel_invoke k provádění paralelních operací

Ukazuje, jak použít algoritmus parallel_invoke pro zlepšení výkonu programu, který provádí více operací ve sdíleném zdroji dat.

Postupy: Vytvoření úlohy, která je dokončena po prodlevě

Ukazuje, jak použít třídy task, cancellation_token_source, cancellation_token a task_completion_event k vytvoření úlohy, která se provede po prodlevě.

Návod: Implementace tříd future

Ukazuje, jak sloučit existující funkce v modulu Runtime souběžnosti do něco jiného s více funkcemi.

Knihovna PPL (Parallel Patterns Library)

Popisuje PPL, který poskytuje imperativní programovací model pro vývoj souběžných aplikací.

Odkaz

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