Delen via


Parallelle uitvoering van taken (gelijktijdigheidsruntime)

In de Gelijktijdigheidsruntime is een taak een werkeenheid die een specifieke taak uitvoert en doorgaans parallel met andere taken wordt uitgevoerd. Een taak kan worden opgesplitst in extra, nauwkeurigere taken die zijn ingedeeld in een taakgroep.

U gebruikt taken wanneer u asynchrone code schrijft en een bewerking wilt uitvoeren nadat de asynchrone bewerking is voltooid. U kunt bijvoorbeeld een taak gebruiken om asynchroon uit een bestand te lezen en vervolgens een andere taak te gebruiken, een vervolgtaak, die verderop in dit document wordt uitgelegd, om de gegevens te verwerken nadat deze beschikbaar zijn. Omgekeerd kunt u takengroepen gebruiken om parallel werk in kleinere delen te splitsen. Stel dat u een recursief algoritme hebt waarmee het resterende werk in twee partities wordt verdeeld. U kunt taakgroepen gebruiken om deze partities gelijktijdig uit te voeren en vervolgens te wachten tot het verdeelde werk is voltooid.

Aanbeveling

Als u dezelfde routine wilt toepassen op elk element van een verzameling parallel, gebruikt u een parallel algoritme, zoals gelijktijdigheid::p arallel_for, in plaats van een taak of taakgroep. Zie Parallelle algoritmen voor meer informatie over parallelle algoritmen.

Belangrijkste punten

  • Wanneer u variabelen naar een lambda-expressie doorgeeft, moet u garanderen dat de levensduur van die variabele blijft bestaan totdat de taak is voltooid.

  • Gebruik taken (de gelijktijdigheid::taakklasse ) wanneer u asynchrone code schrijft. De taakklasse gebruikt De Windows ThreadPool als scheduler, niet de Gelijktijdigheidsruntime.

  • Gebruik taakgroepen (de concurrentie::task_group-klasse of het algoritme concurrentie::parallel_invoke) wanneer u parallel werk in kleinere stukken wilt opdelen en wacht totdat deze kleinere onderdelen zijn voltooid.

  • Gebruik de concurrency::task::then methode om continuaties te maken. Een vervolg is een taak die asynchroon wordt uitgevoerd nadat een andere taak is voltooid. U kunt een willekeurig aantal vervolgen verbinden om een keten van asynchroon werk te vormen.

  • Een vervolg op basis van een taak wordt altijd gepland voor uitvoering wanneer de antecedent-taak is voltooid, zelfs wanneer de antecedent-taak wordt geannuleerd of een uitzondering genereert.

  • Gebruik concurrency::when_all om een taak te maken die wordt voltooid nadat elke taak in de set is voltooid. Gebruik concurrency::when_any om een taak te maken die wordt voltooid zodra één onderdeel van een reeks taken is voltooid.

  • Taken en taakgroepen kunnen deelnemen aan het annuleringsmechanisme van de PPL (Parallel Patterns Library). Zie Annulering in de PPL voor meer informatie.

  • Zie Uitzonderingsafhandeling voor meer informatie over hoe de runtime uitzonderingen verwerkt die worden gegenereerd door taken en taakgroepen.

In dit document

Lambda-expressies gebruiken

Vanwege hun beknopte syntaxis zijn lambda-expressies een veelgebruikte manier om het werk te definiëren dat wordt uitgevoerd door taken en taakgroepen. Hier volgen enkele gebruikstips:

  • Omdat taken doorgaans worden uitgevoerd op achtergrondthreads, moet u rekening houden met de levensduur van het object wanneer u variabelen vastlegt in lambda-expressies. Wanneer u een variabele via waarde vastlegt, wordt er een kopie van die variabele gemaakt in de lambda-lichaam. Wanneer u vastlegt via verwijzing, wordt er geen kopie gemaakt. Zorg er daarom voor dat de levensduur van een variabele die u door verwijzing vastlegt, langer duurt dan de taak die deze gebruikt.

  • Wanneer u een lambda-expressie doorgeeft aan een taak, moet u geen variabelen vastleggen die via referentie op de stapel zijn toegewezen.

  • Wees expliciet over de variabelen die u in lambda-expressies vastlegt, zodat u kunt bepalen wat u vastlegt op waarde versus per verwijzing. Daarom raden we u aan om de [=] of [&] opties voor lambda-expressies niet te gebruiken.

Een veelvoorkomend patroon is wanneer een taak in een vervolgketen wordt toegewezen aan een variabele en een andere taak die variabele leest. U kunt niet vastleggen op waarde omdat elke vervolgtaak een andere kopie van de variabele bevat. Voor stack-toegewezen variabelen kunt u ook niet per verwijzing vastleggen omdat de variabele mogelijk niet meer geldig is.

Als u dit probleem wilt oplossen, gebruikt u een slimme aanwijzer, zoals std::shared_ptr, om de variabele te verpakken en de slimme aanwijzer door te geven op waarde. Op deze manier kan het onderliggende object worden toegewezen en gelezen, en zal het de taken die het gebruiken overleven. Gebruik deze techniek zelfs wanneer de variabele een pointer of een referentiegetelde handle (^) is naar een Windows Runtime-object. Hier volgt een basisvoorbeeld:

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

Zie Lambda-expressies voor meer informatie over lambda-expressies.

De taakklasse

U kunt de gelijktijdigheid::taakklasse gebruiken om taken samen te stellen in een set afhankelijke bewerkingen. Dit samenstellingsmodel wordt ondersteund door het begrip vervolgen. Met een vervolg kan code worden uitgevoerd wanneer de vorige taak, of antecedent, is voltooid. Het resultaat van de antecedent-taak wordt doorgegeven als invoer voor een of meer vervolgtaken. Wanneer een antecedenttaak is voltooid, worden vervolgtaken die erop wachten gepland voor uitvoering. Elke vervolgtaak ontvangt een kopie van het resultaat van de antecedent-taak. Op zijn beurt kunnen deze vervolgtaken ook antecedenttaken zijn voor andere vervolgtaken, waardoor er een keten van taken ontstaat. Vervolgen helpen u bij het maken van willekeurige ketens van taken met specifieke afhankelijkheden. Daarnaast kan een taak deelnemen aan annulering voordat een taak wordt gestart of op een coöperatieve manier terwijl deze wordt uitgevoerd. Zie Annulering in de PPL voor meer informatie over dit annuleringsmodel.

task is een sjabloonklasse. De typeparameter T is het type van het resultaat dat door de taak wordt geproduceerd. Dit type kan zijn void als de taak geen waarde retourneert. T kan de const wijzigingsfunctie niet gebruiken.

Wanneer u een taak maakt, geeft u een werkfunctie op waarmee de hoofdtekst van de taak wordt uitgevoerd. Deze werkfunctie wordt geleverd in de vorm van een lambda-functie, functiepointer of functieobject. Als u wilt wachten tot een taak is voltooid zonder het resultaat te verkrijgen, roept u de gelijktijdigheid::taak::wait-methode aan. De task::wait methode retourneert een gelijktijdigheid::task_status waarde die beschrijft of de taak is voltooid of geannuleerd. Als u het resultaat van de taak wilt ophalen, roept u de concurrency::task::get-methode aan. Met deze methode wordt aangeroepen task::wait om te wachten totdat de taak is voltooid en wordt de uitvoering van de huidige thread geblokkeerd totdat het resultaat beschikbaar is.

In het volgende voorbeeld ziet u hoe u een taak maakt, wacht op het resultaat en de waarde ervan weergeeft. In de voorbeelden in deze documentatie worden lambda-functies gebruikt, omdat ze een beknoptere syntaxis bieden. U kunt echter ook functiepointers en functieobjecten gebruiken wanneer u taken gebruikt.

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

Wanneer u de gelijktijdigheidsfunctie::create_task gebruikt, kunt u het auto trefwoord gebruiken in plaats van het type te declareren. Bekijk bijvoorbeeld deze code waarmee de identiteitsmatrix wordt gemaakt en afgedrukt:

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

U kunt de create_task functie gebruiken om de equivalente bewerking te maken.

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

Als er een uitzondering optreedt tijdens de uitvoering van een taak, verwerkt de runtime die uitzondering in de daaropvolgende aanroep naar task::get of task::wait, of in een taakgebaseerde voortzetting. Zie Uitzonderingsafhandeling voor taken voor meer informatie over het mechanisme voor het afhandelen van taakonderzondering.

Voor een voorbeeld dat gebruik maakt van task, gelijktijdigheid::task_completion_event en annulering, zie Doorloop: Verbinding maken met behulp van taken en XML HTTP-aanvragen. (De task_completion_event klasse wordt verderop in dit document beschreven.)

Aanbeveling

Zie Asynchrone programmering in C++ en Asynchrone bewerkingen maken in C++ voor UWP-apps voor meer informatie over specifieke taken in UWP-apps.

Vervolgtaken

Bij asynchrone programmering is het heel gebruikelijk voor één asynchrone bewerking, bij voltooiing, om een tweede bewerking aan te roepen en er gegevens aan door te geven. Dit wordt traditioneel gedaan met behulp van callback-methoden. In de Gelijktijdigheidsruntime wordt dezelfde functionaliteit geboden door vervolgtaken. Een vervolgtaak (ook wel een vervolg genoemd) is een asynchrone taak die wordt aangeroepen door een andere taak, ook wel de antecedent genoemd, wanneer de antecedent is voltooid. Door vervolgen te gebruiken, kunt u het volgende doen:

  • Geef gegevens van het voorgaande door aan het vervolg.

  • Geef de precieze voorwaarden op waaronder de voortzetting wordt aangeroepen of niet wordt aangeroepen.

  • Annuleer een vervolg voordat deze wordt gestart of coöperatief terwijl deze wordt uitgevoerd.

  • Geef hints over hoe de voortzetting moet worden gepland. (Dit geldt alleen voor UWP-apps (Universal Windows Platform). Zie Asynchrone bewerkingen maken in C++ voor UWP-apps voor meer informatie.

  • Roep meerdere vervolgen aan van dezelfde antecedent.

  • Roep één vervolg aan wanneer alle of een van meerdere antecedenten is voltooid.

  • Ketenvervolgingen één na elkaar tot elke lengte.

  • Gebruik een vervolg om uitzonderingen af te handelen die worden gegenereerd door de antecedent.

Met deze functies kunt u een of meer taken uitvoeren wanneer de eerste taak is voltooid. U kunt bijvoorbeeld een vervolg maken waarmee een bestand wordt gecomprimeerd nadat de eerste taak het van de schijf heeft gelezen.

In het volgende voorbeeld wordt het vorige gewijzigd om de concurrency::task::then methode te gebruiken, waarmee een vervolg gepland wordt dat de waarde van de antecedente taak afdrukt wanneer deze beschikbaar is.

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

U kunt taken in elke gewenste lengte nesten en koppelen. Een taak kan ook meerdere vervolgen hebben. In het volgende voorbeeld ziet u een eenvoudige vervolgketen waarmee de waarde van de vorige taak drie keer wordt verhoogd.

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

Een vervolg kan ook een andere taak teruggeven. Als er geen annulering is, wordt deze taak uitgevoerd vóór de volgende voortzetting. Deze techniek staat bekend als asynchroon uitpakken. Asynchrone uitpakbewerking is handig als u extra werk op de achtergrond wilt uitvoeren, maar niet wilt dat de taak die momenteel wordt uitgevoerd de huidige thread blokkeert. (Dit is gebruikelijk in UWP-apps, waarbij vervolgen kunnen worden uitgevoerd op de UI-thread). In het volgende voorbeeld ziet u drie taken. De eerste taak retourneert een andere taak die wordt uitgevoerd vóór een vervolgtaak.

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

Belangrijk

Wanneer een voortzetting van een taak een geneste taak van het type Nretourneert, heeft de resulterende taak het type N, niet task<N>en wordt voltooid wanneer de geneste taak is voltooid. Met andere woorden, het vervolg voert het uitpakken van de geneste taak uit.

Value-Based Versus Task-Based Voortzettingen

Op basis van een task object waarvan het retourtype T is, kunt u een waarde van het type T of task<T> geven aan de continuatietaken. Een vervolg die type gebruikt T , wordt een vervolg op basis van een waarde genoemd. Een vervolg op basis van een waarde wordt gepland voor uitvoering wanneer de antecedent-taak zonder fouten wordt voltooid en niet wordt geannuleerd. Een vervolg die het type task<T> als parameter gebruikt, wordt een vervolg op basis van een taak genoemd. Een vervolg op basis van een taak wordt altijd gepland voor uitvoering wanneer de antecedent-taak is voltooid, zelfs wanneer de antecedent-taak wordt geannuleerd of een uitzondering genereert. Vervolgens kunt u task::get bellen om het resultaat van de voorgaande taak op te halen. Als de antecedente taak is geannuleerd, task::get gooit concurrency::task_canceled. Als de antecedent-taak een uitzondering heeft veroorzaakt, wordt die uitzondering opnieuw opgeworpen door task::get. Een op een taak gebaseerde voortzetting wordt niet als geannuleerd gemarkeerd wanneer de voorafgaande taak wordt geannuleerd.

Taken samenstellen

In deze sectie worden de gelijktijdigheidsfuncties::when_all en gelijktijdigheid::when_any beschreven, waarmee u meerdere taken kunt opstellen om algemene patronen te implementeren.

De functie when_all

De when_all functie produceert een taak die wordt voltooid nadat een reeks taken is voltooid. Met deze functie wordt een std::vectorobject geretourneerd dat het resultaat van elke taak in de set bevat. Een basisvoorbeeld dat when_all gebruikt om een taak te creëren die staat voor de voltooiing van drie andere taken.

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

Opmerking

De taken die u doorgeeft aan when_all moeten uniform zijn. Met andere woorden, ze moeten namelijk allemaal hetzelfde type retourneren.

U kunt ook de && syntaxis gebruiken om een taak te produceren die wordt voltooid nadat een reeks taken is voltooid, zoals wordt weergegeven in het volgende voorbeeld.

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

Het is gebruikelijk om een vervolg samen met when_all te gebruiken om een actie uit te voeren nadat een reeks taken is afgerond. In het volgende voorbeeld wordt de vorige gewijzigd om de som van drie taken af te drukken die elk een int resultaat opleveren.

// Start multiple tasks.
array<task<int>, 3> tasks =
{
    create_task([]() -> int { return 88; }),
    create_task([]() -> int { return 42; }),
    create_task([]() -> int { return 99; })
};

auto joinTask = when_all(begin(tasks), end(tasks)).then([](vector<int> results)
{
    wcout << L"The sum is " 
          << accumulate(begin(results), end(results), 0)
          << L'.' << endl;
});

// Print a message from the joining thread.
wcout << L"Hello from the joining thread." << endl;

// Wait for the tasks to finish.
joinTask.wait();

/* Output:
    Hello from the joining thread.
    The sum is 229.
*/

In dit voorbeeld kunt u ook opgeven task<vector<int>> dat een vervolg op basis van een taak moet worden geproduceerd.

Als een taak in een set taken wordt geannuleerd of een uitzondering genereert, when_all wordt deze onmiddellijk voltooid en wordt niet gewacht tot de resterende taken zijn voltooid. Als er een uitzondering optreedt, werpt de runtime de uitzondering opnieuw wanneer u task::get of task::wait aanroept op het taakobject dat when_all retourneert. Als er meer dan één taak wordt gegooid, kiest de runtime een van deze taken. Zorg er daarom voor dat u alle uitzonderingen ziet nadat alle taken zijn voltooid; een niet-verwerkte taakonderzondering zorgt ervoor dat de app wordt beëindigd.

Hier volgt een hulpprogrammafunctie die u kunt gebruiken om ervoor te zorgen dat uw programma alle uitzonderingen bekijkt. Voor elke taak in het opgegeven bereik veroorzaakt observe_all_exceptions dat een eventuele opgetreden uitzondering opnieuw wordt geworpen en daarna wordt die uitzondering ingeslikt.

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

Overweeg een UWP-app die gebruikmaakt van C++ en XAML en een set bestanden naar schijf schrijft. In het volgende voorbeeld ziet u hoe u when_all en observe_all_exceptions kunt gebruiken om ervoor te zorgen dat het programma alle uitzonderingen observeert.

// 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();
        }
    });
}
Dit voorbeeld uitvoeren
  1. Voeg een controle toe aan MainPage.xaml.
<Button x:Name="Button1" Click="Button_Click">Write files</Button>
  1. Voeg in MainPage.xaml.h deze doorstuurdeclaraties toe aan de private sectie van de MainPage klassedeclaratie.
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. Implementeer in MainPage.xaml.cpp de Button_Click gebeurtenis-handler.
// 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. Implementeer WriteFilesAsync in MainPage.xaml.cpp, zoals wordt weergegeven in het voorbeeld.

Aanbeveling

when_all is een niet-blokkerende functie die als resultaat een task produceert. In tegenstelling tot taak::wait, is het veilig om deze functie aan te roepen in een UWP-app op de ASTA-thread (Application STA).

De functie when_any

De when_any functie produceert een taak die wordt voltooid wanneer de eerste taak in een reeks taken is voltooid. Deze functie retourneert een std::pair-object dat zowel het resultaat van de voltooide taak als de index van die taak in de set bevat.

De when_any functie is vooral handig in de volgende scenario's:

  • Redundante bewerkingen. Overweeg een algoritme of bewerking die op veel manieren kan worden uitgevoerd. U kunt de when_any functie gebruiken om de bewerking te selecteren die eerst is voltooid en vervolgens de resterende bewerkingen te annuleren.

  • Interleaved bewerkingen. U kunt meerdere bewerkingen starten die allemaal moeten worden voltooid en de when_any functie gebruiken om resultaten te verwerken wanneer elke bewerking is voltooid. Nadat een bewerking is voltooid, kunt u een of meer extra taken starten.

  • Vertraagde bewerkingen. U kunt de when_any functie gebruiken om het vorige scenario uit te breiden door het aantal gelijktijdige bewerkingen te beperken.

  • Verlopen bewerkingen. U kunt de when_any functie gebruiken om te selecteren tussen een of meer taken en een taak die na een bepaalde tijd is voltooid.

Net als bij when_all, is het gebruikelijk om een voortzetting te gebruiken die when_any uitvoert wanneer de eerste van een reeks taken is voltooid. In het volgende basisvoorbeeld wordt een when_any taak gemaakt die wordt voltooid wanneer de eerste van de drie andere taken is voltooid.

// select-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <array>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Start multiple tasks.
    array<task<int>, 3> tasks = {
        create_task([]() -> int { return 88; }),
        create_task([]() -> int { return 42; }),
        create_task([]() -> int { return 99; })
    };

    // Select the first to finish.
    when_any(begin(tasks), end(tasks)).then([](pair<int, size_t> result)
    {
        wcout << "First task to finish returns "
              << result.first
              << L" and has index "
              << result.second
              << L'.' << endl;
    }).wait();
}

/* Sample output:
    First task to finish returns 42 and has index 1.
*/

In dit voorbeeld kunt u ook opgeven task<pair<int, size_t>> dat een vervolg op basis van een taak moet worden geproduceerd.

Opmerking

Net als bij when_all, moeten de taken die u doorgeeft aan when_any allemaal hetzelfde type retourneren.

U kunt ook de || syntaxis gebruiken om een taak te produceren die wordt voltooid nadat de eerste taak in een reeks taken is voltooid, zoals wordt weergegeven in het volgende voorbeeld.

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

Aanbeveling

Net als bij when_all is when_any niet-blokkerend en veilig om aan te roepen in een UWP-app op de ASTA-thread.

Vertraagde taakuitvoering

Het is soms noodzakelijk om de uitvoering van een taak uit te stellen totdat aan een voorwaarde is voldaan of om een taak te starten als reactie op een externe gebeurtenis. In asynchrone programmering moet u bijvoorbeeld een taak starten als reactie op een I/O-voltooiingsbeurtenis.

U kunt dit op twee manieren doen door een vervolg te gebruiken of een taak te starten en te wachten op een gebeurtenis in de werkfunctie van de taak. Er zijn echter gevallen waarin het niet mogelijk is om een van deze technieken te gebruiken. Als u bijvoorbeeld een vervolg wilt maken, moet u de antecedent-taak hebben. Als u echter niet beschikt over de antecedent-taak, kunt u een voltooiingsbeurt voor de taak maken en deze voltooiingsbeurt later koppelen aan de antecedent-taak wanneer deze beschikbaar is. Bovendien kunt u, omdat een wachttaak ook een thread blokkeert, taakvoltooiingsevenementen gebruiken om werk uit te voeren wanneer een asynchrone bewerking is voltooid en zo een thread vrij maakt.

De concurrency::task_completion_event klasse helpt bij het vereenvoudigen van de samenstelling van taken. Net als bij de task klasse is de typeparameter T het type van het resultaat dat door de taak wordt geproduceerd. Dit type kan zijn void als de taak geen waarde retourneert. T kan de const wijzigingsfunctie niet gebruiken. Normaal gesproken wordt een task_completion_event object geleverd aan een thread of taak die aangeeft wanneer de waarde voor het object beschikbaar wordt. Tegelijkertijd worden een of meer taken ingesteld als luisteraars van die gebeurtenis. Wanneer de gebeurtenis is ingesteld, worden de listenertaken voltooid en worden de vervolgtaken gepland om uit te voeren.

Voor een voorbeeld waarbij task_completion_event wordt gebruikt om een taak te implementeren die na een vertraging wordt voltooid, zie Hoe maakt u een taak die na een vertraging wordt voltooid.

Taakgroepen

Een taakgroep organiseert een verzameling taken. Taakgroepen pushen taken naar een werk stelende wachtrij. De scheduler verwijdert taken uit deze wachtrij en voert deze uit op beschikbare rekenresources. Nadat u taken aan een taakgroep hebt toegevoegd, kunt u wachten totdat alle taken zijn voltooid of geannuleerd die nog niet zijn gestart.

De PPL gebruikt de gelijktijdigheid::task_group en gelijktijdigheid::structured_task_group klassen om taakgroepen te vertegenwoordigen en de gelijktijdigheid::task_handle klasse die de taken vertegenwoordigt die in deze groepen worden uitgevoerd. De task_handle klasse bevat de code die werk uitvoert. Net als de task klasse wordt de werkfunctie geleverd in de vorm van een lambda-functie, functiepointer of functieobject. Normaal gesproken hoeft u niet rechtstreeks met task_handle objecten te werken. In plaats daarvan geeft u werkfuncties door aan een taakgroep en de taakgroep maakt en beheert de task_handle objecten.

De PPL verdeelt taakgroepen in deze twee categorieën: ongestructureerde taakgroepen en gestructureerde taakgroepen. De PPL gebruikt de task_group klasse om ongestructureerde taakgroepen en de structured_task_group klasse voor gestructureerde taakgroepen weer te geven.

Belangrijk

De PPL definieert ook het gelijktijdigheidsalgoritme::p arallel_invoke , waarbij de structured_task_group klasse wordt gebruikt om een set taken parallel uit te voeren. Omdat het parallel_invoke algoritme een beknoptere syntaxis heeft, raden we u aan het algoritme te gebruiken in plaats van de structured_task_group klasse wanneer u dat kunt. In het onderwerp Parallelle algoritmen wordt uitgebreider beschreven parallel_invoke .

Gebruik parallel_invoke deze opdracht wanneer u meerdere onafhankelijke taken tegelijk wilt uitvoeren en u moet wachten tot alle taken zijn voltooid voordat u doorgaat. Deze techniek wordt vaak aangeduid als 'fork en join' parallelisme. Gebruik task_group deze opdracht wanneer u meerdere onafhankelijke taken hebt die u tegelijkertijd wilt uitvoeren, maar u wilt wachten totdat de taken op een later tijdstip zijn voltooid. U kunt bijvoorbeeld taken toevoegen aan een task_group object en wachten totdat de taken zijn voltooid in een andere functie of vanuit een andere thread.

Taakgroepen ondersteunen het concept van annulering. Met annulering kunt u aan alle actieve taken aangeven dat u de algehele bewerking wilt annuleren. Annulering voorkomt ook dat taken die nog niet zijn gestart, niet worden gestart. Zie Annulering in de PPL voor meer informatie over annulering.

De runtime biedt ook een uitzonderingsverwerkingsmodel waarmee u een uitzondering van een taak kunt genereren en die uitzondering kunt afhandelen wanneer u wacht tot de bijbehorende taakgroep is voltooid. Zie Uitzonderingsafhandeling voor meer informatie over dit uitzonderingsverwerkingsmodel.

Task_group vergelijken met structured_task_group

Hoewel we aanraden om task_group of parallel_invoke in plaats van de structured_task_group class te gebruiken, zijn er gevallen waarin je structured_task_group wilt gebruiken, bijvoorbeeld wanneer je een parallel algoritme schrijft dat een variabel aantal taken uitvoert of ondersteuning vereist voor annulering. In deze sectie worden de verschillen tussen de task_group en structured_task_group klassen uitgelegd.

De task_group klasse is draadveilig. Daarom kunt u vanuit meerdere threads taken toevoegen aan een task_group object en vanuit meerdere threads op een task_group object wachten of het annuleren. De constructie en vernietiging van een structured_task_group object moeten zich in hetzelfde lexicale bereik bevinden. Bovendien moeten alle bewerkingen op een structured_task_group object plaatsvinden op dezelfde thread. De uitzondering op deze regel zijn de methoden concurrency::structured_task_group::cancel en concurrency::structured_task_group::is_canceling. Een subtaak kan deze methoden aanroepen om de hoofdtaakgroep te annuleren of te allen tijde te controleren op annulering.

U kunt extra taken uitvoeren op een task_group-object nadat u de methode concurrency::task_group::wait of concurrency::task_group::run_and_wait hebt aangeroepen. Integendeel, als u extra taken uitvoert op een structured_task_group object nadat u de methoden concurrency::structured_task_group::wait of concurrency::structured_task_group::run_and_wait hebt aangeroepen, is het gedrag ongedefinieerd.

Omdat de structured_task_group klasse niet wordt gesynchroniseerd tussen threads, heeft deze minder overhead voor de uitvoering dan de task_group klasse. Als uw probleem daarom niet vereist dat u werk plant vanuit meerdere threads en u het parallel_invoke algoritme niet kunt gebruiken, kan de klasse u helpen bij het structured_task_group schrijven van betere prestaties van code.

Als u één structured_task_group object in een ander structured_task_group object gebruikt, moet het binnenste object worden voltooid en vernietigd voordat het buitenste object is voltooid. De task_group klasse vereist niet dat geneste taakgroepen moeten worden voltooid voordat de buitenste groep is voltooid.

Ongestructureerde taakgroepen en gestructureerde taakgroepen werken op verschillende manieren met taakgrepen. U kunt werkfuncties rechtstreeks doorgeven aan een task_group object. Het task_group object maakt en beheert de taakgreep voor u. Voor de structured_task_group klasse moet u een task_handle object voor elke taak beheren. Elk task_handle object moet gedurende de levensduur van het bijbehorende structured_task_group object geldig blijven. Gebruik de functie concurrency::make_task om een task_handle object te maken, zoals weergegeven in een basisvoorbeeld.

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

Als u taakgrepen wilt beheren voor gevallen waarin u een variabel aantal taken hebt, gebruikt u een stacktoewijzingsroutine, zoals _malloca of een containerklasse, zoals std::vector.

Zowel task_group als structured_task_group ondersteunen annulering. Zie Annulering in de PPL voor meer informatie over annulering.

Voorbeeld

In het volgende basisvoorbeeld ziet u hoe u met taakgroepen kunt werken. In dit voorbeeld wordt het parallel_invoke algoritme gebruikt om twee taken gelijktijdig uit te voeren. Met elke taak worden subtaken aan een task_group object toegevoegd. Houd er rekening mee dat de task_group klasse meerdere taken gelijktijdig kan toevoegen.

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

Hieronder ziet u voorbeelduitvoer voor dit voorbeeld:

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

Omdat het parallel_invoke algoritme taken gelijktijdig uitvoert, kan de volgorde van de uitvoerberichten variëren.

Voor volledige voorbeelden die laten zien hoe u het parallel_invoke algoritme gebruikt, raadpleegt u Procedures: Parallel_invoke gebruiken om een parallelle sorteerroutine te schrijven en procedures: gebruik parallel_invoke om parallelle bewerkingen uit te voeren. Zie task_group voor een volledig voorbeeld waarin de klasse wordt gebruikt om asynchrone futures te implementeren.

Robuuste programmering

Zorg ervoor dat u de rol van annulering en uitzonderingsafhandeling begrijpt wanneer u taken, taakgroepen en parallelle algoritmen gebruikt. In een structuur van parallelle werkzaamheden voorkomt een geannuleerde taak bijvoorbeeld dat onderliggende taken worden uitgevoerd. Dit kan problemen veroorzaken als een van de onderliggende taken een bewerking uitvoert die belangrijk is voor uw toepassing, zoals het vrijmaken van een resource. Als een onderliggende taak een uitzondering genereert, kan die uitzondering bovendien worden doorgegeven via een objectdestructor en niet-gedefinieerd gedrag in uw toepassing veroorzaken. Zie voor een voorbeeld dat deze punten illustreert, de sectie Begrijpen hoe annulering en het hanteren van uitzonderingen van invloed zijn op objectvernietiging in het document Beste praktijken in de Parallel Patterns Library. Zie Annulerings - en uitzonderingsafhandelingsmodellen in de PPL voor meer informatie over de annulerings- en uitzonderingsverwerkingsmodellen.

Titel Beschrijving
Procedure: parallel_invoke gebruiken om een parallelle sorteerroutine te schrijven Laat zien hoe u het parallel_invoke algoritme gebruikt om de prestaties van het bitonische sorteeralgoritmen te verbeteren.
Procedure: parallel_invoke gebruiken om parallelle bewerkingen uit te voeren Laat zien hoe u het parallel_invoke algoritme gebruikt om de prestaties van een programma te verbeteren dat meerdere bewerkingen uitvoert op een gedeelde gegevensbron.
Procedure: Een taak maken die na een vertraging wordt voltooid Laat zien hoe u de task, cancellation_token_sourceen cancellation_tokentask_completion_event klassen gebruikt om een taak te maken die na een vertraging wordt voltooid.
Stapsgewijze handleiding: Futures implementeren Laat zien hoe u bestaande functionaliteit in de Gelijktijdigheidsruntime combineert in iets dat meer doet.
Bibliotheek met parallelle patronen (PPL) Beschrijft de PPL, die een imperatief programmeermodel biedt voor het ontwikkelen van gelijktijdige toepassingen.

Referentie

taakklasse (Gelijktijdigheidsruntime)

task_completion_event-klasse

when_all, functie

when_any, functie

task_group-klasse

parallel_invoke, functie

structured_task_group-klasse