Dela via


Aktivitetsparallellitet (Samtidighetskörning)

I Concurrency Runtime är en uppgift en arbetsenhet som utför ett visst jobb och som vanligtvis körs parallellt med andra uppgifter. En uppgift kan delas upp i ytterligare, mer detaljerade uppgifter som är ordnade i en aktivitetsgrupp.

Du använder uppgifter när du skriver asynkron kod och vill att en åtgärd ska utföras när den asynkrona åtgärden har slutförts. Du kan till exempel använda en aktivitet för att asynkront läsa från en fil och sedan använda en annan uppgift – en fortsättningsaktivitet som beskrivs senare i det här dokumentet – för att bearbeta data när den blir tillgänglig. Däremot kan du använda aktivitetsgrupper för att dela upp parallellt arbete i mindre delar. Anta till exempel att du har en rekursiv algoritm som delar upp det återstående arbetet i två partitioner. Du kan använda aktivitetsgrupper för att köra dessa partitioner samtidigt och sedan vänta tills det delade arbetet har slutförts.

Tips/Råd

När du vill tillämpa samma rutin på varje element i en samling parallellt använder du en parallell algoritm, till exempel concurrency::p arallel_for, i stället för en aktivitet eller aktivitetsgrupp. Mer information om parallella algoritmer finns i Parallella algoritmer.

Huvudpunkter

  • När du skickar variabler till ett lambda-uttryck som referens måste du garantera att variabelns livslängd kvarstår tills aktiviteten har slutförts.

  • Använd tasks (concurrency::task-klassen) när du skriver asynkron kod. Aktivitetsklassen använder Windows ThreadPool som schemaläggare, inte Concurrency Runtime.

  • Använd aktivitetsgrupper (concurrency::task_group-klassen eller concurrency::parallel_invoke-algoritmen) när du vill dela upp parallellt arbete i mindre delar och sedan vänta tills de mindre delarna har slutförts.

  • Använd metoden concurrency::task::then för att skapa fortsättningar. En fortsättning är en aktivitet som körs asynkront när en annan aktivitet har slutförts. Du kan ansluta valfritt antal fortsättningar för att bilda en kedja av asynkront arbete.

  • En aktivitetsbaserad fortsättning schemaläggs alltid för körning när den föregående aktiviteten slutförs, även när den föregående aktiviteten avbryts eller utlöser ett undantag.

  • Använd samtidighet::when_all för att skapa en uppgift som slutförs när varje uppgift i en mängd uppgifter har slutförts. Använd concurrency::when_any för att skapa en uppgift som slutförs när en av uppsättningens uppgifter har slutförts.

  • Uppgifter och uppgiftsgrupper kan delta i parallella mönsterbibliotekets avbrytningsmekanism. Mer information finns i Annullering i PPL.

  • Information om hur körningen hanterar undantag som genereras av aktiviteter och aktivitetsgrupper finns i Undantagshantering.

I det här dokumentet

Använda Lambda-uttryck

På grund av deras kortfattade syntax är lambda-uttryck ett vanligt sätt att definiera det arbete som utförs av aktiviteter och aktivitetsgrupper. Här följer några användningstips:

  • Eftersom uppgifter vanligtvis körs på bakgrundstrådar bör du vara medveten om objektets livslängd när du avbildar variabler i lambda-uttryck. När du tilldelar en variabel efter värde, skapas en kopia av variabeln i lambda-uttrycket. När du samlar in med referens görs ingen kopia. Se därför till att livslängden för alla variabler som du samlar in med referens överlever den uppgift som använder den.

  • När du skickar ett lambda-uttryck till en uppgift ska du inte fånga variabler som allokeras på stacken via referens.

  • Var tydlig med de variabler som du samlar in i lambda-uttryck så att du kan identifiera vad du samlar in med värde jämfört med referens. Därför rekommenderar vi att du inte använder [=] alternativen eller [&] för lambda-uttryck.

Ett vanligt mönster är när en aktivitet i en fortsättningskedja tilldelar en variabel och en annan uppgift läser variabeln. Du kan inte samla in efter värde eftersom varje fortsättningsaktivitet skulle innehålla en annan kopia av variabeln. För stackallokerade variabler kan du inte heller avbilda med referens eftersom variabeln kanske inte längre är giltig.

Lös problemet genom att använda en smart pekare, till exempel std::shared_ptr, för att omsluta variabeln och skicka den smarta pekaren efter värde. På så sätt kan det underliggande objektet tilldelas till och läsas från och kommer att överleva de uppgifter som använder det. Använd den här tekniken även när variabeln är en pekare eller ett referensberäkningshandtag (^) till ett Windows Runtime-objekt. Här är ett grundläggande exempel:

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

Mer information om lambda-uttryck finns i Lambda-uttryck.

Uppgiftsklassen

Du kan använda concurrency::task-klassen för att skapa uppgifter i en serie av beroende operationer. Den här kompositionsmodellen stöds av begreppet fortsättningar. En fortsättning gör att kod kan köras när den föregående eller föregående aktiviteten slutförs. Resultatet av den föregående aktiviteten skickas som indata till en eller flera fortsättningsaktiviteter. När en tidigare aktivitet har slutförts planeras eventuella fortsättningsaktiviteter som väntar på den för utförande. Varje fortsättningsaktivitet får en kopia av resultatet av den föregående aktiviteten. Dessa fortsättningsuppgifter kan i sin tur också vara tidigare uppgifter för andra fortsättningar, vilket skapar en kedja av uppgifter. Fortsättningar hjälper dig att skapa godtyckliga längdkedjor med uppgifter som har specifika beroenden bland dem. Dessutom kan en uppgift delta i annullering antingen innan en uppgift startar eller på ett kooperativt sätt medan den körs. Mer information om den här annulleringsmodellen finns i Annullering i PPL.

task är en mallklass. Typparametern T är den typ av resultat som genereras av aktiviteten. Den här typen kan vara void om aktiviteten inte returnerar ett värde. T kan inte använda const modifieraren.

När du skapar en uppgift anger du en arbetsfunktion som utför uppgiftens innehåll. Den här arbetsfunktionen kommer i form av en lambda-funktion, funktionspekare eller funktionsobjekt. Om du vill vänta tills en aktivitet har slutförts utan att hämta resultatet anropar du metoden concurrency::task::wait . Metoden task::wait returnerar ett samtidighetsvärde::task_status som beskriver om aktiviteten slutfördes eller avbröts. Om du vill hämta resultatet av uppgiften anropar du metoden concurrency::task::get . Den här metoden anropar task::wait för att vänta tills uppgiften har slutförts och blockerar därför körningen av den aktuella tråden tills resultatet är tillgängligt.

I följande exempel visas hur du skapar en uppgift, väntar på resultatet och visar dess värde. Exemplen i den här dokumentationen använder lambda-funktioner eftersom de ger en mer kortfattad syntax. Du kan dock också använda funktionspekare och funktionsobjekt när du använder uppgifter.

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

När du använder funktionen concurrency::create_task kan du använda nyckelordet auto i stället för att deklarera typen. Tänk dig till exempel den här koden som skapar och skriver ut identitetsmatrisen:

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

Du kan använda create_task funktionen för att skapa motsvarande åtgärd.

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

Om ett undantag utlöses under körningen av en aktivitet, överför körningsmiljön det undantaget vid ett efterföljande anrop till task::get eller task::wait, eller till en aktivitetsbaserad fortsättning. Mer information om mekanismen för undantagshantering för aktiviteter finns i Undantagshantering.

Ett exempel som använder task, samtidighet::task_completion_event, annullering, finns i Genomgång: Ansluta med uppgifter och XML HTTP-begäranden. (Klassen task_completion_event beskrivs senare i det här dokumentet.)

Tips/Råd

Mer information om uppgifter i UWP-appar finns i Asynkron programmering i C++ och Skapa asynkrona åtgärder i C++ för UWP-appar.

Fortsättningsuppgifter

I asynkron programmering är det mycket vanligt att en asynkron åtgärd, när den är klar, anropar en andra åtgärd och skickar data till den. Traditionellt görs detta med hjälp av återanropsmetoder. I Concurrency Runtime tillhandahålls samma funktionalitet av continuation tasks. En fortsättningsaktivitet (även känd som en fortsättning) är en asynkron aktivitet som anropas av en annan aktivitet, som kallas föregångare, när föregångaren slutförs. Genom att använda fortsättningar kan du:

  • Skicka data från det föregående steget till nästa steg.

  • Ange de exakta villkor under vilka fortsättningen anropas eller inte anropas.

  • Avbryt en fortsättning antingen innan den startar eller medan den pågår.

  • Ange tips om hur fortsättningen ska schemaläggas. (Detta gäller endast UWP-appar (Universal Windows Platform). Mer information finns i Skapa asynkrona åtgärder i C++ för UWP-appar.)

  • Anropa flera fortsättningar från samma föregångare.

  • Anropa en fortsättning när alla eller någon av flera antecedenter har slutförts.

  • Koppla fortsättningar på varandra till vilken längd som helst.

  • Använd en fortsättning för att hantera undantag som kastas av föregångaren.

Med de här funktionerna kan du köra en eller flera uppgifter när den första uppgiften är klar. Du kan till exempel skapa en fortsättning som komprimerar en fil när den första uppgiften har läst den från disken.

I följande exempel ändras det föregående för att använda samtidighet::uppgift::sedan-metoden för att schemalägga en fortsättning som skriver ut värdet för den tidigare uppgiften när den är tillgänglig.

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

Du kan länka och kapsla uppgifter till vilken längd som helst. En aktivitet kan också ha flera fortsättningar. I följande exempel visas en grundläggande fortsättningskedja som ökar värdet för föregående aktivitet tre gånger.

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

En fortsättning kan också returnera en annan uppgift. Om det inte finns någon annullering körs den här uppgiften före den efterföljande fortsättningen. Den här tekniken kallas asynkron omskrivning. Asynkront avpackande är användbart när du vill utföra mer arbete i bakgrunden, men inte vill att den aktuella uppgiften ska blockera den aktuella tråden. (Detta är vanligt i UWP-appar, där fortsättningar kan köras på användargränssnittstråden). I följande exempel visas tre uppgifter. Den första aktiviteten returnerar en annan aktivitet som körs före en fortsättningsaktivitet.

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

Viktigt!

När en fortsättning av en aktivitet returnerar en kapslad aktivitet av typen Nhar den resulterande aktiviteten typen N, inte task<N>och slutförs när den kapslade aktiviteten slutförs. Med andra ord är det fortsättningen som packar upp den kapslade uppgiften.

Value-Based kontra Task-Based-fortsättningar

Med tanke på ett task objekt vars returtyp är Tkan du ange ett värde av typen T eller task<T> till dess fortsättningsuppgifter. En fortsättning som tar typ T kallas för en värdebaserad fortsättning. En värdebaserad fortsättning schemaläggs för körning när den tidigare aktiviteten slutförs utan fel och inte avbryts. En fortsättning som tar typ task<T> som parameter kallas för en aktivitetsbaserad fortsättning. En aktivitetsbaserad fortsättning schemaläggs alltid för körning när den föregående aktiviteten slutförs, även när den föregående aktiviteten avbryts eller utlöser ett undantag. Du kan sedan anropa task::get för att hämta resultatet av den föregående uppgiften. Om den föregående uppgiften avbröts, task::get utlöser samtidighet::task_canceled. Om den föregående aktiviteten utlöste ett undantag, task::get kastar om det undantaget. En aktivitetsbaserad fortsättning markeras inte som avbruten när dess tidigare uppgift avbryts.

Skapa uppgifter

I det här avsnittet beskrivs funktionerna concurrency::when_all och concurrency::when_any, som kan hjälpa dig att sammanställa flera uppgifter för att implementera vanliga mönster.

Funktionen when_all

Funktionen when_all genererar en uppgift som slutförs när en uppsättning aktiviteter har slutförts. Den här funktionen returnerar ett std::vector-objekt som innehåller resultatet av varje uppgift i uppsättningen. I följande grundläggande exempel används when_all för att skapa en uppgift som representerar slutförandet av tre andra aktiviteter.

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

Anmärkning

De uppgifter som du skickar till when_all måste vara enhetliga. Med andra ord måste alla returnera samma typ.

Du kan också använda syntaxen && för att skapa en aktivitet som slutförs efter att en uppsättning aktiviteter har slutförts, som du ser i följande exempel.

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

Det är vanligt att använda en fortsättning tillsammans med when_all för att utföra en åtgärd när en uppsättning uppgifter har slutförts. I följande exempel ändras den föregående för att skriva ut summan av tre uppgifter som var och en ger ett int resultat.

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

I det här exemplet kan du också ange task<vector<int>> för att skapa en aktivitetsbaserad fortsättning.

Om någon aktivitet i en uppsättning aktiviteter avbryts eller utlöser ett undantag when_all slutförs omedelbart och väntar inte på att de återstående aktiviteterna ska slutföras. Om ett undantag kastas, kastar körningen undantaget igen när du anropar task::get eller task::wait på det aktivitetsobjekt som when_all returnerar. Om fler än en uppgift utlöser väljer körningen en av dem. Se därför till att du observerar alla undantag när alla uppgifter har slutförts. ett ohanterat aktivitetsfel gör att appen avslutas.

Här är en verktygsfunktion som du kan använda för att säkerställa att programmet observerar alla undantag. För varje uppgift i det angivna intervallet orsakar observe_all_exceptions att alla undantag som inträffade kastas på nytt och sedan fångas det undantaget.

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

Överväg en UWP-app som använder C++ och XAML och skriver en uppsättning filer till disk. I följande exempel visas hur du använder when_all och observe_all_exceptions för att säkerställa att programmet observerar alla undantag.

// 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();
        }
    });
}
Så här kör du det här exemplet
  1. Lägg till en Button kontroll i MainPage.xaml.
<Button x:Name="Button1" Click="Button_Click">Write files</Button>
  1. I MainPage.xaml.h lägger du till dessa framåtdeklarationer i private avsnittet i klassdeklarationen 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);
  1. Implementera händelsehanteraren i Button_Click MainPage.xaml.cpp.
// 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. I MainPage.xaml.cpp implementerar du WriteFilesAsync enligt exemplet.

Tips/Råd

when_all är en icke-blockerande funktion som skapar en task som resultat. Till skillnad från uppgift::väntaär det säkert att anropa den här funktionen i en UWP-app i ASTA-tråden (Application STA).

Funktionen when_any

Funktionen when_any genererar en aktivitet som slutförs när den första aktiviteten i en uppsättning aktiviteter slutförs. Den här funktionen returnerar ett std::pair-objekt som innehåller resultatet av den slutförda uppgiften och indexet för den uppgiften i mängden.

Funktionen when_any är särskilt användbar i följande scenarier:

  • Redundanta åtgärder. Överväg en algoritm eller åtgärd som kan utföras på många sätt. Du kan använda when_any funktionen för att välja den åtgärd som slutförs först och sedan avbryta de återstående åtgärderna.

  • Interleaverande åtgärder. Du kan starta flera åtgärder som alla måste slutföras och använda when_any funktionen för att bearbeta resultat när varje åtgärd är klar. När en åtgärd är klar kan du starta en eller flera ytterligare uppgifter.

  • Begränsade operationer. Du kan använda when_any funktionen för att utöka det tidigare scenariot genom att begränsa antalet samtidiga åtgärder.

  • Åtgärder som har upphört att gälla. Du kan använda when_any funktionen för att välja mellan en eller flera aktiviteter och en aktivitet som avslutas efter en viss tid.

Precis som med when_all är det vanligt att använda en fortsättning som har when_any för att utföra en åtgärd när det första i en uppsättning uppgifter slutförs. I följande grundläggande exempel används when_any för att skapa en uppgift som slutförs när den första av tre andra aktiviteter slutförs.

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

I det här exemplet kan du också ange task<pair<int, size_t>> för att skapa en aktivitetsbaserad fortsättning.

Anmärkning

Precis som med when_allmåste de uppgifter som du skickar till when_any returnera samma typ.

Du kan också använda syntaxen || för att skapa en aktivitet som slutförs efter att den första aktiviteten i en uppsättning aktiviteter har slutförts, enligt följande exempel.

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

Tips/Råd

Precis som med when_all, when_any är icke-blockerande och är säkert att anropa i en UWP-app på ASTA-tråden.

Fördröjd uppgiftsutförande

Ibland är det nödvändigt att fördröja körningen av en aktivitet tills ett villkor är uppfyllt eller att starta en aktivitet som svar på en extern händelse. I asynkron programmering kan du till exempel behöva starta en uppgift som svar på en I/O-slutförandehändelse.

Två sätt att åstadkomma detta är att använda en fortsättning eller starta en aktivitet och vänta på en händelse i aktivitetens arbetsfunktion. Det finns dock fall där det inte går att använda någon av dessa tekniker. Om du till exempel vill skapa en fortsättning måste du ha den föregående uppgiften. Men om du inte har den föregående uppgiften kan du skapa en händelse för slutförande av aktiviteten och senare länka den slutförandehändelsen till den tidigare aktiviteten när den blir tillgänglig. Eftersom en väntande uppgift även blockerar en tråd kan du dessutom använda händelser för slutförande av aktiviteter för att utföra arbete när en asynkron åtgärd slutförs och därmed frigöra en tråd.

Klassen concurrency::task_completion_event förenklar sammansättningen av uppgifter. Precis som task klassen är typparametern T den typ av resultat som genereras av aktiviteten. Den här typen kan vara void om aktiviteten inte returnerar ett värde. T kan inte använda const modifieraren. Vanligtvis tillhandahålls ett task_completion_event objekt till en tråd eller uppgift som signalerar det när värdet för det blir tillgängligt. Samtidigt anges en eller flera uppgifter som händelsens lyssnare. När händelsen har angetts slutförs lyssnaruppgifterna och deras fortsättningar schemaläggs att köras.

Ett exempel som använder task_completion_event för att implementera en uppgift som slutförs efter en fördröjning finns i Så här skapar du en uppgift som slutförs efter en fördröjning.

Aktivitetsgrupper

En aktivitetsgrupp organiserar en samling aktiviteter. Arbetsgrupper lägger uppgifter på en jobbstöldskö. Schemaläggaren tar bort uppgifter från den här kön och kör dem på tillgängliga beräkningsresurser. När du har lagt till aktiviteter i en aktivitetsgrupp kan du vänta tills alla aktiviteter har slutförts eller avbrutit aktiviteter som ännu inte har startats.

PPL använder samtidighet::task_group och samtidighet::structured_task_group klasser för att representera aktivitetsgrupper och samtidighet::task_handle-klassen för att representera de aktiviteter som körs i dessa grupper. Klassen task_handle kapslar in koden som utför arbete. task Liksom klassen kommer arbetsfunktionen i form av en lambda-funktion, funktionspekare eller funktionsobjekt. Du behöver vanligtvis inte arbeta med task_handle objekt direkt. I stället skickar du arbetsfunktioner till en aktivitetsgrupp, och aktivitetsgruppen skapar och hanterar objekten task_handle .

PPL delar in aktivitetsgrupper i dessa två kategorier: ostrukturerade aktivitetsgrupper och strukturerade aktivitetsgrupper. PPL använder task_group klassen för att representera ostrukturerade aktivitetsgrupper och structured_task_group klassen för att representera strukturerade aktivitetsgrupper.

Viktigt!

PPL definierar också algoritmen concurrency::parallel_invoke, som använder structured_task_group klassen för att köra en uppsättning uppgifter parallellt. Eftersom algoritmen parallel_invoke har en mer kortfattad syntax rekommenderar vi att du använder den i stället för klassen structured_task_group när du kan. Ämnet Parallella algoritmer beskriver parallel_invoke mer detaljerat.

Använd parallel_invoke när du har flera oberoende uppgifter som du vill köra samtidigt och du måste vänta tills alla aktiviteter har slutförts innan du fortsätter. Den här tekniken kallas ofta för fork-och-join parallellism. Använd task_group när du har flera oberoende uppgifter som du vill köra samtidigt, men du vill vänta tills aktiviteterna har slutförts vid ett senare tillfälle. Du kan till exempel lägga till uppgifter i ett task_group objekt och vänta tills aktiviteterna har slutförts i en annan funktion eller från en annan tråd.

Aktivitetsgrupper stöder begreppet annullering. Med annullering kan du signalera till alla aktiva uppgifter eller processer att du vill avbryta hela operationen. Annullering förhindrar också att aktiviteter som ännu inte har startats startar. Mer information om annullering finns i Annullering i PPL.

Körtiden innehåller också en undantagshanteringsmodell som möjliggör att du kan kasta ett undantag från en uppgift och hantera det undantaget när du väntar på att den associerade uppgiftsgruppen ska bli slutförd. Mer information om den här undantagshanteringsmodellen finns i Undantagshantering.

Jämföra task_group med structured_task_group

Även om vi rekommenderar att du använder task_group eller parallel_invoke i stället för structured_task_group klassen, finns det fall där du vill använda structured_task_group, till exempel när du skriver en parallell algoritm som utför ett variabelt antal uppgifter eller kräver stöd för annullering. I det här avsnittet förklaras skillnaderna mellan klasserna task_group och structured_task_group .

Klassen task_group är trådsäker. Därför kan du lägga till uppgifter i ett task_group objekt från flera trådar och vänta på eller avbryta ett task_group objekt från flera trådar. Konstruktionen och förstörelsen av ett structured_task_group objekt måste ske inom samma lexikala omfång. Dessutom måste alla åtgärder på ett structured_task_group objekt utföras i samma tråd. Undantaget från den här regeln är metoderna concurrency::structured_task_group::cancel och concurrency::structured_task_group::is_canceling. En underordnad aktivitet kan anropa dessa metoder för att avbryta den överordnade aktivitetsgruppen eller när som helst söka efter avbokning.

Du kan köra ytterligare uppgifter på ett task_group objekt när du anropar metoden concurrency::task_group::wait eller concurrency::task_group::run_and_wait . Om du däremot kör ytterligare uppgifter på ett structured_task_group-objekt efter att du anropat metoderna samtidighetens::structured_task_group::wait eller samtidighetens::structured_task_group::run_and_wait, är beteendet odefinierat.

structured_task_group Eftersom klassen inte synkroniseras mellan trådar har den mindre körningskostnader än task_group klassen. Om problemet inte kräver att du schemalägger arbete från flera trådar och du inte kan använda algoritmen parallel_invokestructured_task_group kan klassen därför hjälpa dig att skriva kod med bättre prestanda.

Om du använder ett objekt inuti ett structured_task_group annat structured_task_group objekt måste det inre objektet avslutas och förstöras innan det yttre objektet är klart. Klassen task_group kräver inte att kapslade aktivitetsgrupper slutförs innan den yttre gruppen är klar.

Ostrukturerade aktivitetsgrupper och strukturerade aktivitetsgrupper hanterar uppgifter på olika sätt. Du kan skicka arbetsfunktioner direkt till ett task_group objekt. task_group Objektet skapar och hanterar uppgiftshandtaget åt dig. Klassen structured_task_group kräver att du hanterar ett task_handle objekt för varje uppgift. Varje task_handle objekt måste vara giltigt under hela livslängden för det associerade structured_task_group objektet. Använd funktionen concurrency::make_task för att skapa ett task_handle objekt, som du ser i följande grundläggande exempel:

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

Om du vill hantera uppgiftshandtag för fall där du har ett variabelt antal aktiviteter använder du en stackallokeringsrutin, till exempel _malloca eller en containerklass, till exempel std::vector.

Både task_group och structured_task_group stödjer annullering. Mer information om annullering finns i Annullering i PPL.

Exempel

Följande grundläggande exempel visar hur du arbetar med aktivitetsgrupper. I det här exemplet används algoritmen parallel_invoke för att utföra två uppgifter samtidigt. Varje uppgift lägger till underaktiviteter i ett task_group objekt. Observera att task_group-klassen gör det möjligt för flera uppgifter att läggas till samtidigt.

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

Följande är exempelutdata för det här exemplet:

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

Eftersom algoritmen parallel_invoke kör uppgifter samtidigt kan ordningen på utdatameddelandena variera.

Fullständiga exempel som visar hur du använder algoritmen parallel_invoke finns i How to: Use parallel_invoke to Write a Parallel Sort Routine and How to: Use parallel_invoke to Execute Parallel Operations (Använda parallel_invoke för att skriva en parallell sorteringsrutin ) och Så här använder du parallel_invoke för att köra parallella åtgärder. Ett komplett exempel som använder task_group klassen för att implementera asynkrona framtider finns i Genomgång: Implementera framtider.

Robust Programmering

Se till att du förstår rollen för annullering och undantagshantering när du använder uppgifter, aktivitetsgrupper och parallella algoritmer. Till exempel, i ett träd med parallellt arbete, förhindrar en uppgift som avbryts förhindrar underordnade uppgifter från att köras. Detta kan orsaka problem om en av de underordnade uppgifterna utför en åtgärd som är viktig för ditt program, till exempel att frigöra en resurs. Om en underordnad uppgift dessutom utlöser ett undantag kan undantaget spridas via en objektförstörare och orsaka odefinierat beteende i ditt program. Ett exempel som illustrerar dessa punkter finns i avsnittet Förstå hur annullering och undantagshantering påverkar objektförstörelse i dokumentet Metodtips i biblioteket Parallella mönster. Mer information om modeller för annullering och undantagshantering i PPL finns i Annullering och undantagshantering.

Titel Beskrivning
Gör så här: Använd parallel_invoke för att skriva en parallell sorteringsrutin Visar hur du använder algoritmen parallel_invoke för att förbättra prestanda för bitonisk sorteringsalgoritm.
Gör så här: Använd parallel_invoke för att köra parallella åtgärder Visar hur du använder algoritmen parallel_invoke för att förbättra prestandan för ett program som utför flera åtgärder på en delad datakälla.
Gör så här: Skapa en uppgift som slutförs efter en fördröjning Visar hur du använder klasserna task, cancellation_token_source, cancellation_tokenoch task_completion_event för att skapa en uppgift som slutförs efter en fördröjning.
Genomgång: Implementera futures Visar hur du kombinerar befintliga funktioner i Concurrency Runtime till något som gör mer.
PPL (Parallel Patterns Library) Beskriver PPL, som tillhandahåller en imperativ programmeringsmodell för att utveckla samtidiga program.

Hänvisning

Task-klass (Concurrency Runtime)

task_completion_event class

when_all funktion

when_any Funktion

task_group-klass

parallel_invoke funktion

structured_task_group-klass