Condividi tramite


Annullamento nella libreria PPL

In questo argomento viene illustrato il ruolo dell'annullamento nella libreria PPL (Parallel Patterns Library), come annullare un lavoro parallelo e come determinare quando un gruppo di attività è annullato.

Sezioni

  • Strutture ad albero del lavoro parallelo

  • Annullamento delle attività in parallelo

  • Annullamento degli algoritmi paralleli

  • Casi i cui non è consigliabile utilizzare l'annullamento

Strutture ad albero del lavoro parallelo

La libreria PPL utilizza i gruppi di attività per gestire attività e calcoli in modo accurato. È possibile annidare i gruppi di attività in modo formare strutture ad albero del lavoro parallelo. Nella figura seguente viene illustrata una struttura ad albero del lavoro parallelo. In questa illustrazione tg1 e tg2 rappresentano i gruppi di attività, mentre t1, t2, t3, t4 e t5 rappresentano le attività.

Struttura ad albero del lavoro parallelo

Nell'esempio seguente viene illustrato il codice necessario per creare la struttura ad albero dell'illustrazione. In questo esempio tg1 e tg2 sono gli oggetti Concurrency::structured_task_group, mentre t1, t2, t3, t4 e t5 sono gli oggetti Concurrency::task_handle.

// task-tree.cpp
// compile with: /c /EHsc
#include <ppl.h>
#include <sstream>
#include <iostream>
#include <sstream>

using namespace Concurrency;
using namespace std;

void create_task_tree()
{   
   // Create a task group that serves as the root of the tree.
   structured_task_group tg1;

   // Create a task that contains a nested task group.
   auto t1 = make_task([&] {
      structured_task_group tg2;

      // Create a child task.
      auto t4 = make_task([&] {
         // TODO: Perform work here.
      });

      // Create a child task.
      auto t5 = make_task([&] {
         // TODO: Perform work here.
      });

      // Run the child tasks and wait for them to finish.
      tg2.run(t4);
      tg2.run(t5);
      tg2.wait();
   });

   // Create a child task.
   auto t2 = make_task([&] {
      // TODO: Perform work here.
   });

   // Create a child task.
   auto t3 = make_task([&] {
      // TODO: Perform work here.
   });

   // Run the child tasks and wait for them to finish.
   tg1.run(t1);
   tg1.run(t2);
   tg1.run(t3);
   tg1.wait();   
}

[vai all'inizio]

Annullamento delle attività in parallelo

Sono disponibili due modi per annullare un lavoro parallelo. Un modo consiste nel chiamare il metodo Concurrency::task_group::cancel o il metodo Concurrency::structured_task_group::cancel. L'altro consiste nel generare un'eccezione nel corpo di una funzione lavoro dell'attività.

Il metodo cancel è più efficiente della gestione delle eccezioni per annullare una struttura ad albero del lavoro parallelo. Il metodo cancel annulla un gruppo di attività e tutti i gruppi di attività figlio in ordine sequenziale dall'alto verso il basso. La gestione delle eccezioni funziona invece in ordine sequenziale dal basso verso l'alto e deve annullare ogni gruppo di attività figlio in modo indipendente poiché l'eccezione si propaga verso l'alto.

Nelle sezioni seguenti viene illustrato come utilizzare il metodo cancel e la gestione delle eccezioni per annullare il lavoro parallelo. Per ulteriori esempi in cui vengono annullate le attività in parallelo, vedere Procedura: utilizzare l'annullamento per interrompere un ciclo Parallel e Procedura: utilizzare la gestione delle eccezion per interrompere un ciclo Parallel.

Utilizzo del metodo cancel per annullare un lavoro parallelo

I metodi Concurrency::task_group::cancel e Concurrency::structured_task_group::cancel impostano un gruppo di attività sullo stato annullato.

Nota

Il runtime utilizza la gestione delle eccezioni per implementare l'annullamento. Non rilevare o gestire queste eccezioni nel codice. Inoltre, si consiglia di scrivere codice indipendente dalle eccezioni nei corpi delle funzioni per le attività. Ad esempio, è possibile utilizzare il modello Resource Acquisition Is Initialization (RAII) per assicurarsi che le risorse vengono gestite correttamente quando viene generata un'eccezione nel corpo di un'attività. Per un esempio completo che utilizza il modello RAII per eseguire la pulizia di una risorsa in un'attività annullabile, vedere Procedura dettagliata: rimozione di lavoro da un thread dell'interfaccia utente.

Dopo avere chiamato cancel, il gruppo di attività non avvia attività successive. I metodi cancel possono essere chiamati da più attività figlio. Un'attività annullata determina la restituzione di Concurrency::canceled da parte dei metodi Concurrency::task_group::wait e Concurrency::structured_task_group::wait.

Il metodo cancel influisce solo sulle attività figlio. Se, ad esempio, si annulla il gruppo di attività tg1 nell'illustrazione della struttura ad albero del lavoro parallelo, saranno interessate tutte le attività della struttura ad albero (t1, t2, t3, t4 e t5). Se si annulla il gruppo di attività annidato, tg2, saranno interessate solo le attività t4 e t5.

Quando si chiama il metodo cancel, vengono annullati anche tutti i gruppi di attività figlio. Tuttavia, l'annullamento non influisce sugli elementi padre del gruppo di attività di una struttura ad albero del lavoro parallelo. Negli esempi seguenti viene illustrata tale condizione in base all'illustrazione della struttura ad albero del lavoro parallelo.

Nel primo di questi esempi viene creata una funzione lavoro per l'attività t4, attività figlio del gruppo di attività tg2. La funzione lavoro chiama la funzione work in un ciclo. Se una chiamata a work non riesce, l'attività annulla il relativo gruppo di attività padre, determinando il passaggio allo stato annullato del gruppo di attività tg2 ma senza annullare il gruppo di attività tg1.

auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel the parent task
      // and break from the loop.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg2.cancel();
         break;
      }
   }         
});

Il secondo esempio è simile al primo, ad eccezione del fatto che l'attività annulla il gruppo di attività tg1. Questa operazione ha effetto su tutte le attività della struttura ad albero (t1, t2, t3, t4 e t5).

auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel all tasks in the tree.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg1.cancel();
         break;
      }
   }   
});

La classe structured_task_group non è thread-safe. Pertanto, un'attività figlio che chiama un metodo del relativo oggetto structured_task_group padre produce un comportamento non specificato. Le eccezioni a questa regola sono rappresentate dai metodi structured_task_group::cancel e Concurrency::structured_task_group::is_canceling. Un'attività figlio può chiamare questi metodi per annullare l'attività padre o verificarne l'annullamento.

Utilizzo delle eccezioni per annullare un lavoro parallelo

Nell'argomento Gestione delle eccezioni nel runtime di concorrenza viene illustrato il modo in cui il runtime di concorrenza utilizza le eccezioni per comunicare gli errori. Tuttavia, non tutte le eccezioni indicano un errore. Un algoritmo di ricerca potrebbe, ad esempio, annullare il gruppo di attività associato quando viene trovato il risultato. Tuttavia, come indicato in precedenza, la gestione delle eccezioni è meno efficiente dell'utilizzo del metodo cancel per annullare un lavoro parallelo.

Quando si genera un'eccezione nel corpo di una funzione lavoro passata a un gruppo di attività, il runtime archivia l'eccezione e ne esegue il marshalling nel contesto in attesa del completamento del gruppo di attività. Analogamente al metodo cancel, il runtime elimina tutte le attività non ancora avviate e non accetta nuove attività.

Il terzo esempio è simile al secondo, ad eccezione del fatto che l'attività t4 genera un'eccezione per annullare il gruppo di attività tg2. In questo esempio viene utilizzato un blocco try-catch per verificare l'annullamento quando il gruppo di attività tg2 attende il completamento delle relative attività figlio. Analogamente al primo esempio, viene determinato il passaggio allo stato annullato del gruppo di attività tg2 ma senza annullare il gruppo di attività tg1.

structured_task_group tg2;

// Create a child task.      
auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, throw an exception to 
      // cancel the parent task.
      bool succeeded = work(i);
      if (!succeeded)
      {
         throw exception("The task failed");
      }
   }         
});

// Create a child task.
auto t5 = make_task([&] {
   // TODO: Perform work here.
});

// Run the child tasks.
tg2.run(t4);
tg2.run(t5);

// Wait for the tasks to finish. The runtime marshals any exception
// that occurs to the call to wait.
try
{
   tg2.wait();
}
catch (const exception& e)
{
   wcout << e.what() << endl;
}

Nel quarto esempio viene utilizzata la gestione delle eccezioni per annullare l'intera struttura ad albero del lavoro. In questo esempio l'eccezione viene rilevata quando il gruppo di attività tg1 attende il completamento delle relative attività figlio anziché quando il gruppo di attività tg2 attende le relative attività figlio. Analogamente al secondo esempio, questa condizione determina il passaggio allo stato annullato di entrambi i gruppi di attività della struttura ad albero, tg1 e tg2.

// Run the child tasks.
tg1.run(t1);
tg1.run(t2);
tg1.run(t3);   

// Wait for the tasks to finish. The runtime marshals any exception
// that occurs to the call to wait.
try
{
   tg1.wait();
}
catch (const exception& e)
{
   wcout << e.what() << endl;
}

Poiché i metodi task_group::wait e structured_task_group::wait vengono generati quando un'attività figlio genera un'eccezione, non viene ricevuto alcun valore restituito.

Determinazione dell'annullamento di un lavoro

L'annullamento è cooperativo. Pertanto, non si verifica immediatamente. Se un gruppo di attività viene annullato, le chiamate da ogni attività figlio nel runtime possono attivare un punto di interruzione e il runtime genererà e rileverà un tipo di eccezione interno per annullare le attività attive. Il runtime di concorrenza non definisce punti di interruzione specifici; si possono verificare in qualsiasi chiamata al runtime. Il runtime deve gestire le eccezioni generate per poter eseguire l'annullamento. Pertanto, non gestire le eccezioni sconosciute nel corpo di un'attività.

Se un'attività figlio esegue un'operazione che richiede molto tempo e non viene chiamata nel runtime, deve verificare periodicamente l'annullamento e uscire in modo tempestivo. Nell'esempio seguente viene illustrato un modo per determinare l'annullamento di un lavoro. L'attività t4 annulla il gruppo di attività padre quando rileva un errore. L'attività t5 chiama occasionalmente il metodo structured_task_group::is_canceling per verificare l'annullamento. Se il gruppo di attività padre è annullato, l'attività t5 visualizza un messaggio e viene chiusa.

structured_task_group tg2;

// Create a child task.
auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel the parent task
      // and break from the loop.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg2.cancel();
         break;
      }
   }
});

// Create a child task.
auto t5 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // To reduce overhead, occasionally check for 
      // cancelation.
      if ((i%100) == 0)
      {
         if (tg2.is_canceling())
         {
            wcout << L"The task was canceled." << endl;
            break;
         }
      }

      // TODO: Perform work here.
   }
});

// Run the child tasks and wait for them to finish.
tg2.run(t4);
tg2.run(t5);
tg2.wait();

In questo esempio viene verificato l'annullamento ogni 100iterazioni del ciclo dell'attività. La frequenza con cui viene verificato l'annullamento dipende dalla quantità di lavoro eseguita dall'attività e dalla velocità necessaria alle attività per rispondere all'annullamento.

Se non si dispone dell'accesso all'oggetto gruppo di attività padre, chiamare la funzione Concurrency::is_current_task_group_canceling per determinare se il gruppo di attività padre è stato annullato.

[vai all'inizio]

Annullamento degli algoritmi paralleli

Gli algoritmi paralleli nella libreria PPL, ad esempio Concurrency::parallel_for, si basano sui gruppi di attività. Pertanto, per annullare un algoritmo parallelo, è possibile utilizzare molte delle stesse tecniche.

Negli esempi seguenti vengono illustrati diversi modi per annullare un algoritmo parallelo.

Nell'esempio seguente viene utilizzato il metodo Concurrency::structured_task_group::run_and_wait per chiamare l'algoritmo parallel_for. Il metodo structured_task_group::run_and_wait attende il completamento dell'attività fornita. L'oggetto structured_task_group consente alla funzione lavoro di annullare l'attività.

// To enable cancelation, call parallel_for in a task group.
structured_task_group tg;

task_group_status status = tg.run_and_wait([&] {
   parallel_for(0, 100, [&](int i) {
      // Cancel the task when i is 50.
      if (i == 50)
      {
         tg.cancel();
      }
      else
      {
         // TODO: Perform work here.
      }
   });
});

// Print the task group status.
wcout << L"The task group status is: ";
switch (status)
{
case not_complete:
   wcout << L"not complete." << endl;
   break;
case completed:
   wcout << L"completed." << endl;
   break;
case canceled:
   wcout << L"canceled." << endl;
   break;
default:
   wcout << L"unknown." << endl;
   break;
}

Questo esempio produce l'output che segue.

The task group status is: canceled.

Nell'esempio seguente viene utilizzata la gestione delle eccezioni per annullare un ciclo parallel_for. Il runtime effettua il marshalling dell'eccezione nel contesto di chiamata.

try
{
   parallel_for(0, 100, [&](int i) {
      // Throw an exception to cancel the task when i is 50.
      if (i == 50)
      {
         throw i;
      }
      else
      {
         // TODO: Perform work here.
      }
   });
}
catch (int n)
{
   wcout << L"Caught " << n << endl;
}

Questo esempio produce l'output che segue.

Caught 50

Nell'esempio seguente viene utilizzato un flag booleano per coordinare l'annullamento in un ciclo parallel_for. Viene eseguita ogni attività poiché in questo esempio non viene utilizzato il metodo cancel o la gestione delle eccezioni per annullare il set complessivo di attività. Pertanto, questa tecnica può comportare un sovraccarico maggiore nell'elaborazione rispetto a un meccanismo di annullamento.

// Create a Boolean flag to coordinate cancelation.
bool canceled = false;

parallel_for(0, 100, [&](int i) {
   // For illustration, set the flag to cancel the task when i is 50.
   if (i == 50)
   {
      canceled = true;
   }

   // Perform work if the task is not canceled.
   if (!canceled)
   {
      // TODO: Perform work here.
   }
});

Ogni metodo di annullamento presenta alcuni vantaggi rispetto agli altri. Scegliere il metodo appropriato alle specifiche esigenze.

[vai all'inizio]

Casi i cui non è consigliabile utilizzare l'annullamento

L'utilizzo dell'annullamento è appropriato quando ogni membro di un gruppo di attività correlate può uscire in modo tempestivo. Esistono tuttavia alcuni scenari in cui l'annullamento potrebbe non essere appropriato per l'applicazione. Ad esempio, poiché l'annullamento delle attività è cooperativo, il set complessivo di attività non verrà annullato se un singola attività è bloccata. Se, ad esempio, un'attività non è ancora stata avviata, ma sblocca un'altra attività attiva, non verrà avviata se il gruppo di attività viene annullato. In questo modo, possono verificarsi condizioni di deadlock nell'applicazione. Un altro esempio in cui l'utilizzo dell'annullamento potrebbe non essere appropriato è quando un'attività viene annullata ma la relativa attività figlio esegue un'operazione importante, ad esempio la liberazione di una risorsa. Poiché quando l'attività padre viene annullata viene annullato il set complessivo di attività, tale operazione non verrà eseguita. Per un esempio in cui viene illustrato questo punto, vedere la sezione Come l'annullamento e la gestione delle eccezioni influiscono sull'eliminazione degli oggetti nelle procedure consigliate nell'argomento relativo alla libreria PPL.

[vai all'inizio]

Argomenti correlati

Riferimenti

Classe task_group

Classe structured_task_group

Funzione parallel_for

Cronologia delle modifiche

Data

Cronologia

Motivo

Marzo 2011

È stato aggiunto un altro caso alla sezione Casi i cui non è consigliabile utilizzare l'annullamento.

Miglioramento delle informazioni.