Parallelismo delle attività (runtime di concorrenza)

In questo documento viene descritto il ruolo delle attività e dei gruppi di attività nel runtime di concorrenza. Utilizzare i gruppi di attività quando si dispone di due o più elementi di lavoro indipendenti che si desidera eseguire contemporaneamente. Si supponga, ad esempio, di avere un algoritmo ricorsivo che divide il lavoro rimanente in due partizioni. È possibile utilizzare i gruppi di attività per eseguire tali partizioni contemporaneamente. Al contrario, utilizzare gli algoritmi paralleli come Concurrency::parallel_for quando si desidera applicare la stessa routine a ogni elemento di un insieme in parallelo. Per ulteriori informazioni sugli algoritmi paralleli, vedere Algoritmi paralleli.

Attività e gruppi di attività

Per attività si intende un'unità di lavoro che esegue un processo specifico. Un'attività può in genere essere eseguita in parallelo con altre attività e può essere scomposta in ulteriori attività con granularità più fine. Un gruppo di attività organizza un insieme 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.

La libreria PPL utilizza 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à. La classe task_handle incapsula il codice che esegue il lavoro. Questo codice viene fornito sotto forma di funzione lambda, puntatore a funzione o oggetto funzione e viene spesso definito funzione lavoro. In genere non è necessario utilizzare direttamente gli oggetti task_handle, ma è possibile passare le funzioni lavoro a un gruppo di attività, il quale crea e gestisce gli oggetti task_handle.

La libreria PPL divide i gruppi di attività in due categorie: gruppi di attività non strutturate e gruppi di attività strutturate. La libreria PPL utilizza 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.

Nota importanteImportante

La libreria PPL definisce inoltre l'algoritmo Concurrency::parallel_invoke, che utilizza la classe structured_task_group per eseguire un set di attività in parallelo. Poiché l'algoritmo parallel_invoke presenta una sintassi più concisa, è consigliabile utilizzarlo in alternativa alla classe structured_task_group quando è possibile. Nell'argomento Algoritmi paralleli viene descritto più dettagliatamente parallel_invoke.

Utilizzare parallel_invoke quando si dispone di diverse attività indipendenti che si desidera eseguire contemporaneamente ed è necessario attendere il completamento di tutte le attività prima di continuare. Utilizzare task_group quando si dispone di diverse attività indipendenti che si desidera 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 a tutte le attività attive che si desidera annullare l'operazione globale. L'annullamento impedisce inoltre l'avvio delle attività che non sono ancora avviate. Per ulteriori informazioni sull'annullamento, vedere Annullamento nella libreria 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 ulteriori informazioni sul modello di gestione delle eccezioni, vedere Gestione delle eccezioni nel runtime di concorrenza.

Confronto tra task_group e structured_task_group

Sebbene sia consigliabile utilizzare task_group o parallel_invoke anziché la classe structured_task_group, vi sono casi in cui può essere opportuno utilizzare 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 è rappresentata dai metodi 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 su un oggetto task_group dopo aver chiamato il metodo Concurrency::task_group::wait o Concurrency::task_group::run_and_wait. Al contrario, non è possibile eseguire attività aggiuntive su un oggetto structured_task_group dopo aver chiamato il metodo Concurrency::structured_task_group::wait o Concurrency:: structured_task_group::run_and_wait.

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

Se si utilizza 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 di completare il gruppo esterno.

I gruppi di attività non strutturate e i gruppi di attività strutturate vengono utilizzati 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. Utilizzare la funzione Concurrency::make_task per creare un oggetto task_handle, 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 dell'attività per i casi in cui è presente un numero variabile di attività, utilizzare una routine di allocazione dello stack come _malloca o una classe contenitore come std::vector.

task_group e structured_task_group supportano entrambi l'annullamento. Per ulteriori informazioni sull'annullamento, vedere Annullamento nella libreria PPL.

Esempio

Nell'esempio di base seguente viene illustrato l'utilizzo dei gruppi di attività. In questo esempio viene utilizzato 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 di attività a più attività contemporaneamente.

// 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 utilizzare l'algoritmo parallel_invoke, vedere Procedura: utilizzare parallel_invoke per scrivere una routine di ordinamento in parallelo e Procedura: utilizzare parallel_invoke per eseguire operazioni in parallelo. Per un esempio completo in cui viene utilizzata la classe task_group per l'implementazione di future asincrone, vedere Procedura dettagliata: implementazione di future.

Programmazione robusta

Prima di utilizzare 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, essa può propagarsi tramite un distruttore di oggetti e causare un comportamento non definito nell'applicazione. Per un esempio in cui vengono illustrati questi punti, vedere la sezione Come l'annullamento e la gestione delle eccezioni influiscono sull'eliminazione degli oggetti nelle procedure consigliate del documento relativo alla libreria PPL. Per ulteriori informazioni sui modelli di annullamento e di gestione delle eccezioni nella libreria PPL, vedere Annullamento nella libreria PPL e Gestione delle eccezioni nel runtime di concorrenza.

Argomenti correlati

Riferimento

Classe task_group

Funzione parallel_invoke

Classe structured_task_group

Cronologia delle modifiche

Data

Cronologia

Motivo

Marzo 2011

Aggiunta di informazioni sul ruolo dell'annullamento e della gestione delle eccezioni quando si utilizzano gruppi di attività e algoritmi paralleli.

Miglioramento delle informazioni.

Luglio 2010

Il contenuto è stato riorganizzato.

Miglioramento delle informazioni.

Maggio 2010

Ampliamento delle linee guida.

Miglioramento delle informazioni.