Condividi tramite


Procedure consigliate generali nel runtime di concorrenza

Questo documento descrive le procedure consigliate applicabili a più aree del runtime di concorrenza.

Sezioni

Questo documento contiene le seguenti sezioni:

Usare costrutti di sincronizzazione cooperativi quando possibile

Il runtime di concorrenza fornisce molti costrutti sicuri per la concorrenza che non richiedono un oggetto di sincronizzazione esterno. Ad esempio, la classe concurrency::concurrent_vector fornisce operazioni di accodamento e accesso agli elementi indipendenti dalla concorrenza. In questo caso, i puntatori o gli iteratori sicuri per la concorrenza sono sempre validi. Non è una garanzia di inizializzazione degli elementi o di un ordine attraversamento specifico. Tuttavia, nei casi in cui è necessario l'accesso esclusivo a una risorsa, il runtime fornisce le classi concurrency::critical_section, concurrency::reader_writer_lock e concurrency::event . Questi tipi si comportano in modo cooperativo; Pertanto, l'utilità di pianificazione dell'attività può riallocare le risorse di elaborazione in un altro contesto quando la prima attività attende i dati. Quando possibile, usare questi tipi di sincronizzazione anziché altri meccanismi di sincronizzazione, ad esempio quelli forniti dall'API Windows, che non si comportano in modo cooperativo. Per altre informazioni su questi tipi di sincronizzazione e un esempio di codice, vedere Synchronization Data Structures (Strutture dei dati di sincronizzazione) e Confronto tra le strutture dei dati di sincronizzazione e l'API Windows.

[Torna all'inizio]

Evitare attività lunghe che non producono

Poiché l'utilità di pianificazione dell'attività si comporta in modo cooperativo, non fornisce equità tra le attività. Pertanto, un'attività può impedire l'avvio di altre attività. Anche se questo è accettabile in alcuni casi, in altri casi questo può causare deadlock o fame.

Nell'esempio seguente vengono eseguite più attività rispetto al numero di risorse di elaborazione allocate. La prima attività non restituisce all'utilità di pianificazione dell'attività e pertanto la seconda attività non viene avviata fino al termine della prima attività.

// cooperative-tasks.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

// Data that the application passes to lightweight tasks.
struct task_data_t
{
   int id;  // a unique task identifier.
   event e; // signals that the task has finished.
};

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

int wmain()
{
   // For illustration, limit the number of concurrent 
   // tasks to one.
   Scheduler::SetDefaultSchedulerPolicy(SchedulerPolicy(2, 
      MinConcurrency, 1, MaxConcurrency, 1));

   // Schedule two tasks.

   task_data_t t1;
   t1.id = 0;
   CurrentScheduler::ScheduleTask(task, &t1);

   task_data_t t2;
   t2.id = 1;
   CurrentScheduler::ScheduleTask(task, &t2);

   // Wait for the tasks to finish.

   t1.e.wait();
   t2.e.wait();
}

Nell'esempio viene prodotto l'output seguente:

1: 250000000 1: 500000000 1: 750000000 1: 1000000000 2: 250000000 2: 500000000 2: 750000000 2: 1000000000

Esistono diversi modi per consentire la cooperazione tra le due attività. Un modo consiste nel restituire occasionalmente all'utilità di pianificazione attività in un'attività a esecuzione prolungata. L'esempio seguente modifica la task funzione per chiamare il metodo concurrency::Context::Yield per restituire l'esecuzione all'utilità di pianificazione in modo che un'altra attività possa essere eseguita.

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();

         // Yield control back to the task scheduler.
         Context::Yield();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

Nell'esempio viene prodotto l'output seguente:

1: 250000000
2: 250000000
1: 500000000
2: 500000000
1: 750000000
2: 750000000
1: 1000000000
2: 1000000000

Il Context::Yield metodo restituisce solo un altro thread attivo nell'utilità di pianificazione a cui appartiene il thread corrente, un'attività leggera o un altro thread del sistema operativo. Questo metodo non restituisce il lavoro pianificato per l'esecuzione in un oggetto concurrency::task_group o concurrency::structured_task_group ma non è ancora stato avviato.

Esistono altri modi per consentire la cooperazione tra le attività a esecuzione prolungata. È possibile suddividere un'attività di grandi dimensioni in sottoattività più piccole. È anche possibile abilitare l'oversubscription durante un'attività lunga. L'oversubscription consente di creare un numero di thread superiore a quello dei thread hardware disponibili. L'oversubscription è particolarmente utile quando un'attività lunga contiene una quantità elevata di latenza, ad esempio la lettura di dati dal disco o da una connessione di rete. Per altre informazioni sulle attività leggere e l'oversubscription, vedere Utilità di pianificazione.

[Torna all'inizio]

Usare l'oversubscription per eseguire l'offset delle operazioni che bloccano o hanno una latenza elevata

Il runtime di concorrenza fornisce primitive di sincronizzazione, ad esempio concurrency::critical_section, che consentono alle attività di bloccare in modo cooperativo e restituire tra loro. Quando un'attività blocca o restituisce in modo cooperativo, l'utilità di pianificazione può riallocare le risorse di elaborazione in un altro contesto quando la prima attività attende i dati.

Esistono casi in cui non è possibile usare il meccanismo di blocco cooperativo fornito dal runtime di concorrenza. Ad esempio, una libreria esterna usata potrebbe usare un meccanismo di sincronizzazione diverso. Un altro esempio è quando si esegue un'operazione che potrebbe avere una quantità elevata di latenza, ad esempio quando si usa la funzione API ReadFile di Windows per leggere i dati da una connessione di rete. In questi casi, l'oversubscription può consentire l'esecuzione di altre attività quando un'altra attività è inattiva. L'oversubscription consente di creare un numero di thread superiore a quello dei thread hardware disponibili.

Si consideri la funzione seguente, download, che scarica il file nell'URL specificato. In questo esempio viene usato il metodo concurrency::Context::Oversubscribe per aumentare temporaneamente il numero di thread attivi.

// Downloads the file at the given URL.
string download(const string& url)
{
   // Enable oversubscription.
   Context::Oversubscribe(true);

   // Download the file.
   string content = GetHttpFile(_session, url.c_str());
   
   // Disable oversubscription.
   Context::Oversubscribe(false);

   return content;
}

Poiché la GetHttpFile funzione esegue un'operazione potenzialmente latente, l'oversubscription può consentire l'esecuzione di altre attività quando l'attività corrente attende i dati. Per la versione completa di questo esempio, vedere Procedura: Usare oversubscription per compensare la latenza.

[Torna all'inizio]

Usare le funzioni di gestione della memoria simultanee quando possibile

Usare le funzioni di gestione della memoria, concurrency::Alloc e concurrency::Free, quando sono presenti attività con granularità fine che allocano spesso oggetti di piccole dimensioni che hanno una durata relativamente breve. Il runtime di concorrenza contiene una cache di memoria separata per ogni thread in esecuzione. Le Alloc funzioni e Free allocano e liberano memoria da queste cache senza l'uso di blocchi o barriere di memoria.

Per altre informazioni su queste funzioni di gestione della memoria, vedere Utilità di pianificazione. Per un esempio che usa queste funzioni, vedere Procedura: Usare Alloc e Free per migliorare le prestazioni di memoria.

[Torna all'inizio]

Usare RAII per gestire la durata degli oggetti di concorrenza

Il runtime di concorrenza usa la gestione delle eccezioni per implementare funzionalità come l'annullamento. Pertanto, scrivere codice indipendente dalle eccezioni quando si chiama nel runtime o si chiama un'altra libreria che chiama nel runtime.

Il modello di inizializzazione dell'acquisizione delle risorse (RAII) è un modo per gestire in modo sicuro la durata di un oggetto di concorrenza in un determinato ambito. Nel modello RAII viene allocata una struttura di dati nello stack. Tale struttura di dati inizializza o acquisisce una risorsa quando viene creata e distrugge o rilascia tale risorsa quando la struttura dei dati viene eliminata definitivamente. Il modello RAII garantisce che il distruttore venga chiamato prima dell'uscita dall'ambito di inclusione. Questo modello è utile quando una funzione contiene più return istruzioni. Questo modello consente anche di scrivere codice indipendente dall'eccezione. Quando un'istruzione throw causa la rimozione dello stack, viene chiamato il distruttore per l'oggetto RAII, pertanto la risorsa viene sempre eliminata o rilasciata correttamente.

Il runtime definisce diverse classi che usano il modello RAII, ad esempio concurrency::critical_section::scoped_lock e concurrency::reader_writer_lock::scoped_lock. Queste classi helper sono note come blocchi con ambito. Queste classi offrono diversi vantaggi quando si lavora con gli oggetti concurrency::critical_section o concurrency::reader_writer_lock . Il costruttore di queste classi acquisisce l'accesso all'oggetto o reader_writer_lock fornitocritical_section. Il distruttore rilascia l'accesso a tale oggetto. Poiché un blocco con ambito rilascia automaticamente l'accesso al relativo oggetto di esclusione reciproca quando viene eliminato definitivamente, non si sblocca manualmente l'oggetto sottostante.

Si consideri la classe seguente, account, definita da una libreria esterna e pertanto non può essere modificata.

// account.h
#pragma once
#include <exception>
#include <sstream>

// Represents a bank account.
class account
{
public:
   explicit account(int initial_balance = 0)
      : _balance(initial_balance)
   {
   }

   // Retrieves the current balance.
   int balance() const
   {
      return _balance;
   }

   // Deposits the specified amount into the account.
   int deposit(int amount)
   {
      _balance += amount;
      return _balance;
   }

   // Withdraws the specified amount from the account.
   int withdraw(int amount)
   {
      if (_balance < 0)
      {
         std::stringstream ss;
         ss << "negative balance: " << _balance << std::endl;
         throw std::exception((ss.str().c_str()));
      }

      _balance -= amount;
      return _balance;
   }

private:
   // The current balance.
   int _balance;
};

Nell'esempio seguente vengono eseguite più transazioni su un account oggetto in parallelo. Nell'esempio viene utilizzato un critical_section oggetto per sincronizzare l'accesso all'oggetto account perché la account classe non è indipendente dalla concorrenza. Ogni operazione parallela usa un critical_section::scoped_lock oggetto per garantire che l'oggetto critical_section venga sbloccato quando l'operazione ha esito positivo o negativo. Quando il saldo del conto è negativo, l'operazione withdraw non riesce generando un'eccezione.

// account-transactions.cpp
// compile with: /EHsc
#include "account.h"
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

int wmain()
{
   // Create an account that has an initial balance of 1924.
   account acc(1924);

   // Synchronizes access to the account object because the account class is 
   // not concurrency-safe.
   critical_section cs;

   // Perform multiple transactions on the account in parallel.   
   try
   {
      parallel_invoke(
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before deposit: " << acc.balance() << endl;
            acc.deposit(1000);
            wcout << L"Balance after deposit: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(50);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(3000);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         }
      );
   }
   catch (const exception& e)
   {
      wcout << L"Error details:" << endl << L"\t" << e.what() << endl;
   }
}

Questo esempio produce l'output di esempio seguente:

Balance before deposit: 1924
Balance after deposit: 2924
Balance before withdrawal: 2924
Balance after withdrawal: -76
Balance before withdrawal: -76
Error details:
    negative balance: -76

Per altri esempi che usano il modello RAII per gestire la durata degli oggetti di concorrenza, vedere Procedura dettagliata: Rimozione di un lavoro da un thread dell'interfaccia utente, Procedura: Usare la classe context per implementare un semaforo cooperativo e Procedura: Usare l'oversubscription per compensare la latenza.

[Torna all'inizio]

Non creare oggetti di concorrenza nell'ambito globale

Quando si crea un oggetto di concorrenza in ambito globale, nell'applicazione si possono verificare problemi come il deadlock o violazioni di accesso alla memoria.

Ad esempio, quando si crea un oggetto runtime di concorrenza, tramite il runtime viene creata un'utilità di pianificazione predefinita, se non è già disponibile. Un oggetto runtime creato durante la costruzione di un oggetto globale comporta la creazione di questa utilità di pianificazione predefinita da parte del runtime. Tuttavia, questo processo prevede un blocco interno, che può interferire con l'inizializzazione di altri oggetti che supportano l'infrastruttura del runtime di concorrenza. Questo blocco interno potrebbe essere richiesto da un altro oggetto dell'infrastruttura che non è ancora stato inizializzato e potrebbe verificarsi un deadlock nell'applicazione.

Nell'esempio seguente viene illustrata la creazione di un oggetto concurrency globale ::Scheduler . Questo modello viene applicato non solo alla classe Scheduler, ma anche a tutti gli altri tipi forniti dal runtime di concorrenza. È consigliabile non seguire questo modello, poiché potrebbe causare un comportamento imprevisto nell'applicazione.

// global-scheduler.cpp
// compile with: /EHsc
#include <concrt.h>

using namespace concurrency;

static_assert(false, "This example illustrates a non-recommended practice.");

// Create a Scheduler object at global scope.
// BUG: This practice is not recommended because it can cause deadlock.
Scheduler* globalScheduler = Scheduler::Create(SchedulerPolicy(2,
   MinConcurrency, 2, MaxConcurrency, 4));

int wmain() 
{   
}

Per esempi del modo corretto per creare Scheduler oggetti, vedere Utilità di pianificazione.

[Torna all'inizio]

Non usare oggetti di concorrenza nei segmenti di dati condivisi

Il runtime di concorrenza non supporta l'uso di oggetti di concorrenza in una sezione dei dati condivisi, ad esempio una sezione di dati creata dalla direttiva data_seg#pragma . Un oggetto di concorrenza condiviso tra i limiti del processo potrebbe mettere il runtime in uno stato incoerente o non valido.

[Torna all'inizio]

Vedi anche

Procedure consigliate del runtime di concorrenza
PPL (Parallel Patterns Library)
Libreria di agenti asincroni
Utilità di pianificazione
Strutture di dati di sincronizzazione
Confronto delle strutture di dati di sincronizzazione con l'API Windows
Procedura: Usare Alloc e Free per migliorare le prestazioni di memoria
Procedura: Usare l'oversubscription per compensare la latenza
Procedura: Usare la classe Context per implementare una classe semaforo di cooperazione
Procedura dettagliata: rimozione di lavoro da un thread dell'interfaccia utente
Procedure consigliate nella libreria PPL (Parallel Patterns Library)
Procedure consigliate nella libreria di agenti asincroni