Condividi tramite


Procedure consigliate generali nel runtime di concorrenza

In questo documento vengono illustrate le procedure consigliate che si applicano a più aree del runtime di concorrenza.

Sezioni

Il documento include le sezioni seguenti:

  • Utilizzare i costrutti di sincronizzazione cooperativa quando possibile

  • Evitare attività di lunga durata che non cedono volontariamente il controllo

  • Utilizzare l'oversubscription per compensare le operazioni che si bloccano o prevedono una latenza elevata

  • Utilizzare funzioni di gestione della memoria simultanee quando possibile

  • Utilizzare il modello RAII per gestire la durata degli oggetti di concorrenza

  • Non creare oggetti di concorrenza in ambito globale

  • Non utilizzare gli oggetti di concorrenza in segmenti di dati condivisi

Utilizzare i costrutti di sincronizzazione cooperativa quando possibile

Il runtime di concorrenza fornisce numerosi costrutti indipendenti dalla concorrenza che non richiedono un oggetto di sincronizzazione esterno.Ad esempio, il concurrency::concurrent_vector classe fornisce elemento le operazioni di accesso e aggiungere la concorrenza-safe.Tuttavia, nei casi in cui si richiedono l'accesso esclusivo a una risorsa, il runtime fornisce il concurrency::critical_section, concurrency::reader_writer_lock, e concurrency::event classi.Questi tipi si comportano in modo cooperativo, pertanto l'utilità di pianificazione può riallocare le risorse di elaborazione a un altro contesto mentre la prima attività resta in attesa dei dati.Quando possibile, utilizzare questi tipi di sincronizzazione anziché altri meccanismi di sincronizzazione, ad esempio quelli forniti dalle API Windows, che non si comportano in modo cooperativo.Per ulteriori informazioni su questi tipi di sincronizzazione e per un codice di esempio, vedere Strutture di dati di sincronizzazione e Confronto delle strutture di dati di sincronizzazione con l'API Windows.

Top

Evitare attività di lunga durata che non cedono volontariamente il controllo

Dal momento che l'utilità di pianificazione si comporta in modo cooperativo, non presuppone l'equità tra le attività.Pertanto, un'attività può impedire l'avvio di altre attività.In alcuni casi questa situazione è accettabile ma in altri casi può provocare un deadlock o l'esaurimento delle risorse.

Nell'esempio seguente viene eseguito un numero di attività superiore a quello delle risorse di elaborazione allocate.La prima attività non cede volontariamente il controllo all'utilità di pianificazione delle attività, pertanto la seconda attività viene avviata solo dopo che la prima attività è stata completata.

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

Questo esempio produce il seguente output:

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à.Uno dei sistemi consiste nel cedere il controllo all'utilità di pianificazione nel caso di un'attività di lunga durata.Nell'esempio seguente viene modificato il task funzione da chiamare al concurrency::Context::Yield metodo per ottenere l'esecuzione per l'utilità di pianificazione in modo che un'altra attività può essere eseguito.

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

Questo esempio produce il seguente output:

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

Il metodo Context::Yield cede il controllo per l'esecuzione solo di un altro thread attivo sull'utilità di pianificazione cui appartiene il thread corrente, di un'attività leggera o di un thread di un altro sistema operativo.Questo metodo non produce per il lavoro pianificato per l'esecuzione in un concurrency::task_group o concurrency::structured_task_group oggetto, ma non è ancora iniziata.

Esistono altri modi per consentire la cooperazione tra attività di lunga durata.È possibile suddividere un'attività grande in sottoattività più piccole.Si può inoltre abilitare l'oversubscription durante un'attività di lunga durata.L'oversubscription consente di creare un numero di thread superiore a quello dei thread hardware disponibili.L'oversubscription è particolarmente utile quando un'attività di lunga durata prevede una quantità elevata di latenza, ad esempio la lettura di dati dal disco o da una connessione di rete.Per ulteriori informazioni sulle attività leggere e sull'oversubscription, vedere Utilità di pianificazione (runtime di concorrenza).

Top

Utilizzare l'oversubscription per compensare le operazioni che si bloccano o prevedono una latenza elevata

Il Runtime di concorrenza fornisce le primitive di sincronizzazione, ad esempio concurrency::critical_section, che consentono operazioni per bloccare congiuntamente e resa a altra.Quando una sola attività si blocca o cede il controllo, l'utilità di pianificazione può riallocare le risorse di elaborazione a un altro contesto mentre la prima attività resta in attesa dei dati.

Vi sono casi in cui non è possibile utilizzare il meccanismo di blocco cooperativo offerto dal runtime di concorrenza.Ciò accade ad esempio quando si lavora con una libreria esterna che utilizza un meccanismo di sincronizzazione diverso.Un altro esempio è quando si esegue un'operazione che potrebbe prevedere una quantità elevata di latenza, ad esempio, quando si utilizza la funzione ReadFile dell'API Windows per leggere i dati da una connessione di rete.In questi casi l'oversubscription può abilitare 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 seguente funzione, download, che consente di scaricare il file nell'URL specificato.Questo esempio viene utilizzata la concurrency::Context::Oversubscribe metodo consente di incrementare 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 funzione GetHttpFile esegue un'operazione potenzialmente latente, l'oversubscription può abilitare l'esecuzione di altre attività mentre l'attività corrente resta in attesa dei dati.Per la versione completa di questo esempio, vedere Procedura: utilizzare l'oversubscription per compensare la latenza.

Top

Utilizzare funzioni di gestione della memoria simultanee quando possibile

Utilizzare le funzioni di gestione della memoria, concurrency::Alloc e concurrency::Free, quando si dispone di specifiche attività che spesso allocare oggetti di piccole dimensioni che hanno una durata relativamente breve.Il runtime di concorrenza gestisce una cache di memoria separata per ogni thread in esecuzione.Le funzioni Alloc e Free allocano e liberano memoria da queste cache senza l'utilizzo di blocchi o barriere di memoria.

Per ulteriori informazioni su queste funzioni di gestione della memoria, vedere Utilità di pianificazione (runtime di concorrenza).Per un esempio in cui vengono utilizzate queste funzioni, vedere Procedura: utilizzare Alloc e Free per migliorare le prestazioni di memoria.

Top

Utilizzare il modello RAII per gestire la durata degli oggetti di concorrenza

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

Il modello RAII (Resource Acquisition Is Initialization) rappresenta un modo per gestire correttamente la durata di un oggetto di concorrenza in un ambito specifico.In base al modello RAII, una struttura dei dati viene allocata sullo stack.La struttura dei dati inizializza o acquisisce una risorsa quando viene creata ed elimina o rilascia tale risorsa quando la struttura dei dati viene eliminata.Il modello RAII garantisce che il distruttore venga chiamato prima della chiusura dell'ambito che lo contiene.Questo modello si rivela utile quando una funzione include più istruzioni return.Consente inoltre di scrivere codice indipendente dalle eccezioni.Quando un'istruzione throw determina la rimozione dello stack, viene chiamato il distruttore dell'oggetto RAII, pertanto la risorsa viene sempre eliminata o rilasciata correttamente.

Common Language runtime definisce diverse classi che utilizzano il modello RAII, ad esempio, concurrency::critical_section::scoped_lock e concurrency::reader_writer_lock::scoped_lock.Queste classi di supporto sono note come blocchi con ambito.Queste classi offrono diversi vantaggi quando si lavora con concurrency::critical_section o concurrency::reader_writer_lock oggetti.Il costruttore di queste classi acquisisce l'accesso all'oggetto critical_section oppure reader_writer_lock specificato, mentre il distruttore rilascia l'accesso a tale oggetto.Dal momento che un blocco con ambito rilascia automaticamente l'accesso al rispettivo oggetto a esclusione reciproca quando viene eliminato, l'oggetto sottostante non viene sbloccato manualmente.

Si consideri la seguente classe, account che, in quanto definita da una libreria esterna, 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 in parallelo su un oggetto account.Nell'esempio viene utilizzato un oggetto critical_section per sincronizzare l'accesso all'oggetto account poiché la classe account non è indipendente dalla concorrenza.Ogni operazione parallela utilizza un oggetto critical_section::scoped_lock 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 di 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 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 ulteriori esempi in cui viene utilizzato il modello RAII per gestire la durata degli oggetti di concorrenza, vedere Procedura dettagliata: rimozione di lavoro da un thread dell'interfaccia utente, Procedura: utilizzare la classe Context per implementare una classe semaforo di cooperazione e Procedura: utilizzare l'oversubscription per compensare la latenza.

Top

Non creare oggetti di concorrenza in ambito globale

Quando si crea un oggetto di concorrenza in ambito globale possono causare problemi quali deadlock o la memoria si verificano nell'applicazione le violazioni di accesso.

Ad esempio, quando si crea un oggetto di Runtime di concorrenza, Common Language runtime crea una pianificazione predefinita per l'utente se non è stato ancora creato.Un oggetto di runtime viene creato durante la costruzione di un oggetto globale verrà di conseguenza il runtime creare questa pianificazione predefinita.Tuttavia, questo processo richiede un blocco interno, che può interferire con l'inizializzazione di altri oggetti che supportano l'infrastruttura di Runtime della concorrenza.Questo blocco interno potrebbe essere necessario da un altro oggetto di infrastrutture che non è stato inizializzato e pertanto può causare un deadlock si verifichi nell'applicazione.

Nell'esempio seguente viene illustrata la creazione di un global concurrency::Scheduler oggetto.Questo criterio si applica non solo per la Scheduler classe, ma tutti i tipi forniti da Common Language Runtime della concorrenza.Si consiglia di non seguire questo modello poiché può provocare 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 relativi al modo corretto per creare gli oggetti Scheduler, vedere Utilità di pianificazione (runtime di concorrenza).

Top

Non utilizzare gli oggetti di concorrenza in segmenti di dati condivisi

Il runtime di concorrenza non supporta l'utilizzo di oggetti di concorrenza in una sezione di dati condivisa, ad esempio una sezione dati creata dalla direttiva data_seg#pragma.Un oggetto di concorrenza condiviso nell'ambito dei processi potrebbe impostare uno stato incoerente o non valido per il runtime.

Top

Vedere anche

Attività

Procedura: utilizzare Alloc e Free per migliorare le prestazioni di memoria

Procedura: utilizzare l'oversubscription per compensare la latenza

Procedura: utilizzare la classe Context per implementare una classe semaforo di cooperazione

Procedura dettagliata: rimozione di lavoro da un thread dell'interfaccia utente

Concetti

PPL (Parallel Patterns Library)

Libreria di agenti asincroni

Utilità di pianificazione (runtime di concorrenza)

Strutture di dati di sincronizzazione

Confronto delle strutture di dati di sincronizzazione con l'API Windows

Procedure consigliate nella libreria PPL (Parallel Patterns Library)

Procedure consigliate nella libreria di agenti asincroni

Altre risorse

Procedure consigliate del runtime di concorrenza