Parallelismo delle attività (runtime di concorrenza)

Nel runtime di concorrenza un'attività è un'unità di lavoro che esegue un processo specifico e in genere viene eseguita in parallelo con altre attività. Un'attività può essere scomposta in attività aggiuntive con granularità più fine organizzate in un gruppo di attività.

Usare le attività quando si scrive codice asincrono e si vuole che alcune operazioni si verifichino al completamento dell'operazione asincrona. Ad esempio, è possibile usare un'attività per leggere in modo asincrono da un file e quindi usare un'altra attività, ovvero un'attività di continuazione, illustrata più avanti in questo documento, per elaborare i dati dopo la disponibilità. È possibile, invece, usare i gruppi di attività per scomporre il lavoro parallelo in sezioni più piccole. Si supponga, ad esempio, di avere un algoritmo ricorsivo che divide il lavoro rimanente in due partizioni. È possibile usare i gruppi di attività per eseguire queste partizioni contemporaneamente e quindi attendere che il lavoro diviso venga completato

Suggerimento

Quando si desidera applicare la stessa routine a ogni elemento di una raccolta in parallelo, usare un algoritmo parallelo, ad esempio concurrency::p arallel_for, anziché un'attività o un gruppo di attività. Per altre informazioni sugli algoritmi paralleli, vedere Algoritmi paralleli.

Punti chiave

  • Quando si passano le variabili a un'espressione lambda in base al riferimento, è necessario garantire che tale variabile duri fino al completamento dell'attività.

  • Usare le attività (la classe concurrency::task ) quando si scrive codice asincrono. La classe dell'attività usa il pool di thread di Windows come utilità di pianificazione, non il runtime di concorrenza.

  • Usare i gruppi di attività (la classe concurrency::task_group o l'algoritmo concurrency::p arallel_invoke ) quando si vuole scomporre il lavoro parallelo in parti più piccole e attendere il completamento di tali parti più piccole.

  • Usare il metodo concurrency::task::then per creare le continuazioni. Una continuazione è un'attività che viene eseguita in modo asincrono dopo il completamento di un'altra attività. È possibile connettere qualsiasi numero di continuazioni per formare una catena di lavoro asincrono.

  • Una continuazione basata sulle attività è sempre pianificata per l'esecuzione quando l'attività precedente viene completata, anche quando l'attività precedente viene annullata o genera un'eccezione.

  • Usare concurrency::when_all per creare un'attività che viene completata al termine di ogni membro di un set di attività. Usare concurrency::when_any per creare un'attività che viene completata dopo il completamento di un membro di un set di attività.

  • Il meccanismo di annullamento della libreria PPL (Parallel Patterns Library) coinvolge le attività e i gruppi di attività. Per altre informazioni, vedere Annullamento nel PPL.

  • Per informazioni su come il runtime gestisce le eccezioni generate da attività e gruppi di attività, vedere Gestione delle eccezioni.

In questo documento

Uso di espressioni lambda

Poiché presentano una sintassi concisa, le espressioni lambda sono comunemente usate per definire il lavoro eseguito da attività e gruppi di attività. Ecco alcuni suggerimenti sull'utilizzo:

  • Poiché in genere le attività vengono eseguite su thread in background, tenere tenga presente la durata degli oggetti quando si acquisiscono variabili nelle espressioni lambda. Quando si acquisisce una variabile in base al valore, una copia della variabile viene eseguita nel corpo dell'espressione lambda. Quando l'acquisizione viene fatta in base al riferimento, la copia non viene eseguita. Pertanto, verificare che la durata di qualsiasi variabile acquisita in base al riferimento sia maggiore di quella dell'attività che la usa.

  • Quando si passa un'espressione lambda a un'attività, non acquisire variabili allocate nello stack per riferimento.

  • Essere espliciti sulle variabili acquisite nelle espressioni lambda in modo da poter identificare ciò che si sta acquisendo in base al valore rispetto al riferimento. Per questo motivo si consiglia di non usare le opzioni [=] o [&] per le espressioni lambda

Un modello comune è quello in cui un'attività contenuta in una catena di continuazione viene assegnata a una variabile e un'altra attività legge tale variabile. Non è possibile acquisire in base al valore perché ogni attività di continuazione conterrà una copia diversa della variabile. Per le variabili allocate allo stack, non è anche possibile acquisire in base al riferimento perché la variabile potrebbe non essere più valida.

Per risolvere questo problema, usare un puntatore intelligente, ad esempio std::shared_ptr, per eseguire il wrapping della variabile e passare il puntatore intelligente per valore. In questo modo, l'oggetto sottostante può essere assegnato e letto e sopravvivrà alle attività che lo usano. Usare questa tecnica anche se la variabile è un puntatore o un handle con il numero di riferimenti (^) a un oggetto di Windows Runtime. Ecco un esempio di base:

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

Per altre informazioni sulle espressioni lambda, vedere Espressioni lambda in C++.

Classe task

È possibile usare la classe concurrency::task per comporre attività in un set di operazioni dipendenti. Questo modello di composizione è supportato dalla nozione di continuazioni. Una continuazione consente l'esecuzione del codice quando l'attività precedente o precedente, o precedente, viene completata. Il risultato dell'attività precedente viene passato come input a una o più attività di continuazione. Quando viene completata un'attività antecedente, viene pianificata l'esecuzione di tutte le attività di continuazione in attesa. Ogni attività di continuazione riceve una copia del risultato dell'attività precedente. A loro volta, tali attività di continuazione possono essere attività antecedenti per altre continuazioni e creano, pertanto, una catena di attività. Le continuazioni consentono di creare catene di lunghezza arbitraria delle attività che presentano dipendenze specifiche tra di esse. Inoltre, un'attività può partecipare all'annullamento prima dell'avvio di un'attività o in modo cooperativo mentre è in esecuzione. Per altre informazioni su questo modello di annullamento, vedere Annullamento nel PPL.

task task è una classe modello. Il parametro di tipo T è il tipo del risultato prodotto dall'attività. Questo tipo può essere void se l'attività non restituisce un valore. T non può usare il modificatore const.

Quando si crea un'attività, si fornisce una funzione di lavoro che esegue il corpo dell'attività. Questa funzione di lavoro ha il formato di una funzione lambda, di un puntatore a funzione o di un oggetto funzione. Per attendere il completamento di un'attività senza ottenere il risultato, chiamare il metodo concurrency::task::wait . Il task::wait metodo restituisce un valore concurrency::task_status che descrive se l'attività è stata completata o annullata. Per ottenere il risultato dell'attività, chiamare il metodo concurrency::task::get . Questo metodo chiama task::wait per attendere il completamento dell'attività e quindi blocca l'esecuzione del thread corrente finché non è disponibile il risultato.

Nell'esempio seguente viene illustrato come creare un'attività, attenderne il risultato e visualizzarne il valore. Negli esempi di questa documentazione vengono usate le funzioni lambda in quanto forniscono una sintassi più concisa. Tuttavia, quando si usano le attività è anche possibile usare i puntatori a funzione e gli oggetti funzione.

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

Quando si usa la funzione concurrency::create_task , è possibile usare la auto parola chiave anziché dichiarare il tipo. Ad esempio, si consideri il codice che crea e visualizza la matrice di identità:

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

È possibile usare la funzione create_task per creare l'operazione equivalente.

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

Se viene generata un'eccezione durante l'esecuzione di un'attività, il runtime effettua il marshalling di tale eccezione nella chiamata successiva a task::get o task::wait o su una continuazione basata sull'attività. Per altre informazioni sul meccanismo di gestione delle eccezioni dell'attività, vedere Gestione delle eccezioni.

Per un esempio che usa task, concurrency::task_completion_event, l'annullamento, vedere Procedura dettagliata: Connessione tramite attività e richieste HTTP XML. (La classe task_completion_event è descritta più avanti in questo documento).

Suggerimento

Per informazioni dettagliate specifiche per le attività nelle app UWP, vedi Programmazione asincrona in C++ e Creazione di operazioni asincrone in C++ per le app UWP.

Attività di continuazione

Nella programmazione asincrona è molto comune che un'operazione asincrona, al completamento, richiami una seconda operazione e vi passi i dati. A tale scopo, si usano in genere i metodi di callback. Nel runtime di concorrenza, la stessa funzionalità viene fornita dalle attività di continuazione. Un'attività di continuazione (nota anche come continuazione) è un'attività asincrona richiamata da un'altra attività, nota come precedente, al completamento dell'attività precedente. Usando le continuazioni è possibile:

  • Passare dati dall'attività precedente alla continuazione.

  • Specificare le esatte condizioni che devono verificarsi affinché la continuazione venga richiamata o meno.

  • Annullare una continuazione prima che venga avviata o in modo cooperativo mentre è in esecuzione.

  • Fornire suggerimenti sul modo in cui pianificare la continuazione. Questo vale solo per le app piattaforma UWP (Universal Windows Platform) (UWP). Per altre informazioni, vedi Creazione di operazioni asincrone in C++ per le app UWP.

  • Richiamare più continuazioni dalla stessa attività precedente.

  • Richiamare una determinata continuazione quando tutte o alcune attività precedenti vengono completate.

  • Concatenare continuazioni una dopo l'altra fino a raggiungere una lunghezza qualsiasi.

  • Usare una continuazione per gestire le eccezioni generate dall'attività precedente.

Queste funzionalità consentono di eseguire una o più attività al completamento della prima attività. Ad esempio, è possibile creare una continuazione che comprime un file dopo che è stato letto dal disco dalla prima attività.

Nell'esempio seguente viene modificato quello precedente per usare il metodo concurrency::task::then per pianificare una continuazione che stampa il valore dell'attività precedente quando è disponibile.

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

È possibile concatenare e annidare attività in qualsiasi lunghezza. Un'attività può anche avere più continuazioni. Nell'esempio seguente viene illustrata una catena di continuazione di base che incrementa il valore dell'attività precedente di tre volte.

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

Una continuazione può restituire anche un'altra attività. Se non è specificato un annullamento, questa attività viene eseguita prima della successiva continuazione. Questa tecnica è nota come rimozione asincrona del wrapping. L'annullamento del wrapping asincrono è utile quando si vuole eseguire il lavoro aggiuntivo in background senza che l'attività corrente blocchi il thread corrente. Questa operazione è comune nelle app UWP, in cui le continuazioni possono essere eseguite nel thread dell'interfaccia utente. L'esempio seguente riporta tre attività. La prima attività restituisce un'altra attività che viene eseguita prima di un'attività di continuazione.

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

Importante

Quando una continuazione di un'attività restituisce un'attività annidata di tipo N, l'attività risultante ha il tipo N, non task<N>, e viene completata al completamento dell'attività annidata. In altre parole, la continuazione annulla il wrapping dell'attività annidata.

Continuazioni basate su valori e basate su attività

Dato un oggetto task il cui tipo restituito è T, è possibile specificare un valore di tipo T o task<T> alle relative attività di continuazione. Una continuazione che accetta il tipo T è nota come continuazione basata su valori. Una continuazione basata su valore viene programmata per essere eseguita quando l'attività antecedente viene completata senza errori e non viene annullata. Una continuazione che accetta il tipo task<T> come parametro è nota come continuazione basata su attività. Una continuazione basata sulle attività è sempre pianificata per l'esecuzione quando l'attività precedente viene completata, anche quando l'attività precedente viene annullata o genera un'eccezione. È quindi possibile chiamare task::get per ottenere il risultato dell'attività precedente. Se l'attività precedente è stata annullata, task::get genera concurrency::task_canceled. Se l'attività precedente ha generato un'eccezione, task::get genera nuovamente tale eccezione. Una continuazione basata sulle attività non è contrassegnata come annullata quando la relativa attività precedente viene annullata.

Composizione di attività

Questa sezione descrive le funzioni concurrency::when_all e concurrency::when_any , che consentono di comporre più attività per implementare modelli comuni.

Funzione when_all

La funzione when_all crea un'attività che viene completata dopo il completamento di un set di attività. Questa funzione restituisce un oggetto std::vector che contiene il risultato di ogni attività nel set. Nell'esempio di base riportato di seguito viene usato when_all per creare un'attività che rappresenta il completamento di altre tre attività.

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

Nota

Le attività passate a when_all devono essere uniformi. In altre parole, devono restituire tutte lo stesso tipo.

È inoltre possibile usare la sintassi && per creare un'attività che venga completata dopo il completamento di un set di attività, come illustrato nell'esempio seguente.

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

Viene di solito usata una continuazione con when_all per eseguire un'azione quando un set di attività viene completato. Nell'esempio seguente viene modificato l'esempio precedente in modo da stampare la somma di tre attività ciascuna delle quali produce un risultato 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.
*/

In questo esempio, è inoltre possibile specificare task<vector<int>> per produrre una continuazione basata sull'attività.

Se un'attività in un set di attività viene annullata o genera un'eccezione, when_all viene completata immediatamente senza attendere il completamento delle attività rimanenti. Se viene generata un'eccezione, il runtime genera nuovamente l'eccezione quando si chiama task::get o task::wait nell'oggetto attività restituito da when_all. Se viene generata più di un'attività, il runtime ne sceglie una. Pertanto, assicurarsi di osservare tutte le eccezioni dopo il completamento di tutte le attività; un'eccezione di attività non gestita causa la chiusura dell'applicazione.

Ecco una funzione di utilità che è possibile usare per assicurarsi che il programma osservi tutte le eccezioni. Per ogni attività nell'intervallo specificato, observe_all_exceptions attiva qualsiasi eccezione che si è verificata affinché venga generata nuovamente, quindi elimina l'eccezione.

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

Si consideri un'app UWP che usa C++ e XAML e scrive un set di file su disco. Nell'esempio seguente viene illustrato come usare when_all e observe_all_exceptions per garantire l'osservanza di tutte le eccezioni da parte del programma.

// 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();
        }
    });
}
Per eseguire questo esempio
  1. In MainPage.xaml, aggiungere un controllo Button.
<Button x:Name="Button1" Click="Button_Click">Write files</Button>
  1. In MainPage.xaml.h, aggiungere le seguenti dichiarazioni con prototipo alla sezione private della dichiarazione di classe 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. In MainPage.xaml.cpp, implementare il gestore eventi 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.
    });
}
  1. In MainPage.xaml.cpp, implementare WriteFilesAsync come mostrato nell'esempio.

Suggerimento

when_all è una funzione non bloccante che produce task come risultato. A differenza di task::wait, è possibile chiamare questa funzione in un'app UWP nel thread ASTA (Application STA).

Funzione when_any

La funzione when_any crea un'attività che viene completata al completamento della prima attività di un set di attività. Questa funzione restituisce un oggetto std::p air che contiene il risultato dell'attività completata e l'indice di tale attività nel set.

La funzione when_any è particolarmente utile nei seguenti scenari:

  • Operazioni ridondanti. Si consideri un algoritmo o un'operazione eseguibile in molti modi. È possibile usare la funzione when_any per selezionare l'operazione che termina per prima e quindi annullare le operazioni rimanenti.

  • Operazioni interfogliate. È possibile avviare più operazioni, che devono tutte venire completate e usare la funzione when_any per elaborare i risultati al termine di ogni operazione. Al termine di un'operazione, è possibile avviare una o più attività aggiuntive.

  • Operazioni con limitazione. È possibile usare la funzione when_any per estendere lo scenario precedente limitando il numero di operazioni simultanee.

  • Operazioni scadute. È possibile usare la funzione when_any per scegliere tra una o più attività e un'attività che termina dopo un periodo specifico.

Come con when_all, viene di solito usata una continuazione con when_any per eseguire un'azione quando le prime di un set di attività sono state completate. Nell'esempio di base riportato di seguito viene usato when_any per creare un'attività che viene completata al completamento della prima delle altre tre attività.

// 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 questo esempio, è inoltre possibile specificare task<pair<int, size_t>> per produrre una continuazione basata sull'attività.

Nota

Come con when_all, le attività passate a when_any devono restituire tutte lo stesso tipo.

È inoltre possibile usare la sintassi || per creare un'attività che venga completata dopo il completamento della prima attività di un set di attività, come illustrato nell'esempio seguente.

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

Suggerimento

Come con when_all, when_any non blocca e è sicuro da chiamare in un'app UWP sul thread ASTA.

Esecuzione attività ritardata

A volte è necessario ritardare l'esecuzione di un'attività fino a soddisfare una condizione oppure avviare un'attività in risposta a un evento esterno. Ad esempio, nella programmazione asincrona potrebbe essere necessario avviare un'attività in risposta a un evento di completamento I/O.

A tale scopo, è possibile usare una continuazione o avviare un'attività e attendere un evento all'interno della funzione di lavoro dell'attività. In alcuni casi, tuttavia, non è possibile usare una di queste tecniche. Ad esempio, per creare una continuazione, è necessario avere l'attività antecedente. Tuttavia, se non si dispone dell'attività precedente, è possibile creare un evento di completamento dell'attività e successiva concatenare tale evento di completamento all'attività precedente quando diventa disponibile. Inoltre, poiché un'attività in attesa blocca anche un thread, è possibile usare eventi di completamento di attività per eseguire il lavoro quando un'operazione asincrona viene completata e quindi libera un thread.

La classe concurrency::task_completion_event consente di semplificare tale composizione di attività. Analogamente ala classe task, il parametro di tipo T è il tipo del risultato prodotto dall'attività. Questo tipo può essere void se l'attività non restituisce un valore. T non può usare il modificatore const. In genere, un oggetto task_completion_event viene fornito a un thread o a un'attività che lo segnalerà se diventa disponibile il valore per l'oggetto. Contemporaneamente, una o più attività vengono impostate come listener di tale evento. Quando viene impostato l'evento, le attività del listener vengono completate e viene pianificata l'esecuzione delle loro continuazioni.

Per un esempio che usa task_completion_event per implementare un'attività che viene completata dopo un ritardo, vedere Procedura: Creare un'attività che viene completata dopo un ritardo.

Gruppi di attività

Un gruppo di attività organizza una raccolta di attività. I gruppi di attività inseriscono le attività in una coda di acquisizione del lavoro. L'utilità di pianificazione rimuove le attività da questa coda eseguendole nelle risorse di elaborazione disponibili. Dopo avere aggiunto le attività a un gruppo di attività, è possibile attendere il completamento di tutte le attività o l'annullamento delle attività che non sono ancora state avviate.

Il PPL usa le classi concurrency::task_group e concurrency::structured_task_group per rappresentare i gruppi di attività e la classe concurrency::task_handle per rappresentare le attività eseguite in questi gruppi. La classe task_handle incapsula il codice che esegue il lavoro. Come la classe task, questa funzione di lavoro ha il formato di una funzione lambda, di un puntatore a funzione o di un oggetto funzione. In genere non è necessario usare direttamente gli oggetti task_handle, ma è possibile passare le funzioni lavoro a un gruppo di attività, che crea e gestisce gli oggetti task_handle.

La libreria PPL divide i gruppi di attività in queste due categorie: gruppi di attività non strutturati e gruppi di attività strutturati. La libreria PPL usa la classe task_group per rappresentare i gruppi di attività non strutturate e la classe structured_task_group per rappresentare i gruppi di attività strutturate.

Importante

Il PPL definisce anche l'algoritmo concurrency::p arallel_invoke , che usa la structured_task_group classe per eseguire un set di attività in parallelo. Poiché l'algoritmo parallel_invoke presenta una sintassi più concisa, è consigliabile usarlo in alternativa alla classe structured_task_group quando è possibile. L'argomento Algoritmi paralleli descrive parallel_invoke in modo più dettagliato.

Usare parallel_invoke quando sono presenti diverse attività indipendenti che si vuole eseguire contemporaneamente ed è necessario attendere il completamento di tutte le attività prima di continuare. Questa tecnica viene spesso definita fork e parallelismo di join . Usare task_group quando sono presenti diverse attività indipendenti che si vuole eseguire contemporaneamente ma è possibile attendere il completamento delle attività in un secondo momento. È possibile, ad esempio, aggiungere attività a un oggetto task_group e attendere il completamento delle attività in un'altra funzione o da parte di un altro thread.

I gruppi di attività supportano il concetto di annullamento. L'annullamento consente di segnalare l'annullamento dell'operazione globale a tutte le attività attive. L'annullamento impedisce inoltre l'avvio delle attività che non sono ancora avviate. Per altre informazioni sull'annullamento, vedere Annullamento in PPL.

Il runtime fornisce inoltre un modello di gestione delle eccezioni che consente di generare un'eccezione da un'attività e di gestire tale eccezione durante l'attesa del completamento del gruppo di attività associato. Per altre informazioni su questo modello di gestione delle eccezioni, vedere Gestione delle eccezioni.

Confronto tra task_group e structured_task_group

Sebbene sia consigliabile usare task_group o parallel_invoke anziché la classe structured_task_group, in alcuni casi può essere opportuno usare structured_task_group, ad esempio quando si scrive un algoritmo parallelo che esegue un numero variabile di attività o che richiede il supporto per l'annullamento. In questa sezione vengono illustrate le differenze tra le classi task_group e structured_task_group.

La classe task_group è thread-safe. È possibile pertanto aggiungere attività a un oggetto task_group da più thread e attendere o annullare un oggetto task_group da più thread. La costruzione e la distruzione di un oggetto structured_task_group devono essere eseguite nello stesso ambito lessicale. Inoltre, tutte le operazioni su un oggetto structured_task_group devono essere eseguite nello stesso thread. L'eccezione a questa regola è il metodo concurrency::structured_task_group::cancel e concurrency::structured_task_group::is_canceling . Un'attività figlio può chiamare questi metodi per annullare il gruppo di attività padre o verificarne l'annullamento in qualsiasi momento.

È possibile eseguire attività aggiuntive in un task_group oggetto dopo aver chiamato il metodo concurrency::task_group::wait o concurrency::task_group::run_and_wait . Viceversa, se si eseguono attività aggiuntive su un structured_task_group oggetto dopo aver chiamato i metodi concurrency::structured_task_group::wait o concurrency::structured_task_group::run_and_wait , il comportamento non è definito.

Poiché la classe structured_task_group non viene sincronizzata nei thread, ha un sovraccarico di esecuzione inferiore rispetto alla classe task_group. Pertanto, se il problema non richiede la pianificazione del lavoro da più thread e non è possibile usare l'algoritmo parallel_invoke, la classe structured_task_group consente di scrivere un codice dalle prestazioni migliori.

Se si usa un oggetto structured_task_group all'interno di un altro oggetto structured_task_group, è necessario che l'oggetto interno venga completato ed eliminato prima del completamento dell'oggetto esterno. La classe task_group non richiede il completamento dei gruppi di attività annidate prima del completamento del gruppo esterno.

I gruppi di attività non strutturate e i gruppi di attività strutturate vengono usati con gli handle dell'attività in diversi modi. È possibile passare le funzioni lavoro direttamente a un oggetto task_group. L'oggetto task_group creerà e gestirà l'handle dell'attività automaticamente. Con la classe structured_task_group è necessario gestire un oggetto task_handle per ogni attività. Ogni oggetto task_handle deve rimanere valido per tutta la durata del relativo oggetto structured_task_group associato. Usare la funzione concurrency::make_task per creare un task_handle oggetto, come illustrato nell'esempio di base seguente:

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

Per gestire gli handle di attività per i casi in cui si dispone di un numero variabile di attività, usare una routine di allocazione dello stack, ad esempio _malloca o una classe contenitore, ad esempio std::vector.

task_group e structured_task_group supportano entrambi l'annullamento. Per altre informazioni sull'annullamento, vedere Annullamento in PPL.

Esempio

Nell'esempio di base seguente viene illustrato l'uso dei gruppi di attività. In questo esempio viene usato l'algoritmo parallel_invoke per eseguire due attività contemporaneamente. Ogni attività aggiunge sottoattività a un oggetto task_group. Si noti che la classe task_group consente l'aggiunta simultanea di attività a più attività.

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

Questo esempio produce l'output seguente:

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

Poiché l'algoritmo parallel_invoke esegue le attività contemporaneamente, l'ordine dei messaggi di output potrebbe variare.

Per esempi completi che illustrano come usare l'algoritmo parallel_invoke , vedere Procedura: Usare parallel_invoke per scrivere una routine di ordinamento parallelo e Procedura: Usare parallel_invoke per eseguire operazioni parallele. Per un esempio completo che usa la task_group classe per implementare future asincrone, vedere Procedura dettagliata: Implementazione di futures.

Programmazione efficiente

Prima di usare le attività, i gruppi di attività e gli algoritmi paralleli, assicurarsi di aver compreso il ruolo dell'annullamento e della gestione delle eccezioni. Ad esempio, in un albero di lavoro parallelo l'annullamento di un'attività impedisce l'esecuzione delle attività figlio. Ciò può comportare problemi se una delle attività figlio esegue un'operazione importante per l'applicazione, ad esempio liberare una risorsa. Inoltre, se un'attività figlio genera un'eccezione, questa può propagarsi tramite un distruttore di oggetti e causare un comportamento indefinito nell'applicazione. Per un esempio che illustra questi punti, vedere la sezione Informazioni su come l'annullamento e la gestione delle eccezioni influiscono sulla distruzione degli oggetti nel documento Procedure consigliate della raccolta di modelli paralleli. Per altre informazioni sui modelli di annullamento e gestione delle eccezioni in PPL, vedere Annullamento e gestione delle eccezioni.

Posizione Descrizione
Procedura: Usare parallel_invoke per scrivere una routine di ordinamento in parallelo Viene illustrato come usare l'algoritmo parallel_invoke per migliorare le prestazioni dell'algoritmo di ordinamento bitonico.
Procedura: Usare parallel_invoke per eseguire operazioni in parallelo Viene illustrato come usare l'algoritmo parallel_invoke per migliorare le prestazioni di un programma che esegue più operazioni in un'origine dati condivisa.
Procedura: Creare un'attività che viene completata dopo un ritardo Illustra come usare le taskclassi , cancellation_token_source, cancellation_tokene task_completion_event per creare un'attività che viene completata dopo un ritardo.
Procedura dettagliata: implementazione di future Viene illustrato come combinare le funzionalità esistenti del runtime di concorrenza con funzionalità ancora più avanzate.
PPL (Parallel Patterns Library) Viene descritta la libreria PPL che fornisce un modello di programmazione imperativa per lo sviluppo di applicazioni simultanee.

Riferimento

Classe task (runtime di concorrenza)

Classe task_completion_event

Funzione when_all

Funzione when_any

Classe task_group

Funzione parallel_invoke

Classe structured_task_group