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, la classe Concurrency::concurrent_vector consente operazioni di accodamento e di accesso agli elementi in modo indipendente dalla concorrenza. Tuttavia per i casi in cui si richiede l'accesso esclusivo a una risorsa, il runtime offre le classi Concurrency::critical_section, Concurrency::reader_writer_lock e Concurrency::event. 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.
[vai all'inizio]
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 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à. Uno dei sistemi consiste nel cedere il controllo all'utilità di pianificazione nel caso di un'attività di lunga durata. Nell'esempio riportato di seguito la funzione task viene modificata per chiamare il metodo Concurrency::Context::Yield in modo che venga lasciato il controllo dell'esecuzione all'utilità di pianificazione per consentire l'esecuzione di un'altra attività.
// 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 l'output seguente:
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 cede il controllo a lavoro la cui esecuzione in un oggetto Concurrency::task_group oppure Concurrency::structured_task_group è stata pianificata ma non ancora avviata.
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).
[vai all'inizio]
Utilizzare l'oversubscription per compensare le operazioni che si bloccano o prevedono una latenza elevata
Il runtime di concorrenza fornisce le primitive di sincronizzazione, come Concurrency::critical_section, che consentono alle attività di bloccarsi e cedersi reciprocamente il controllo in modo cooperativo. 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. In questo esempio viene utilizzato 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 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.
[vai all'inizio]
Utilizzare funzioni di gestione della memoria simultanee quando possibile
Utilizzare le funzioni di gestione della memoria, Concurrency::Alloc e Concurrency::Free, nel caso di attività ad elevata precisione che allocano con una certa frequenza piccoli oggetti la cui 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.
[vai all'inizio]
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.
Il 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. Tali classi offrono diversi vantaggi quando si lavora con l'oggetto Concurrency::critical_section oppure Concurrency::reader_writer_lock. 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.
[vai all'inizio]
Non creare oggetti di concorrenza in ambito globale
Se si crea un oggetto di concorrenza in ambito globale, può verificarsi un deadlock nell'applicazione.
Quando si crea un oggetto runtime di concorrenza, il runtime crea un'utilità di pianificazione predefinita se non ne è ancora stata creata una. Ciò accade anche per un oggetto runtime creato durante la costruzione di un oggetto globale. Tuttavia, questo processo prevede un blocco interno, che può interferire con l'inizializzazione di altri oggetti che supportano l'infrastruttura del runtime di concorrenza. Poiché questo blocco interno potrebbe essere richiesto da un altro oggetto dell'infrastruttura che non è stato ancora inizializzato, è possibile che nell'applicazione si verifichi un deadlock.
Nell'esempio riportato di seguito viene illustrata la creazione di un oggetto Concurrency::Scheduler globale. Questo modello viene applicato non solo alla classe Scheduler, ma anche a tutti gli altri tipi forniti dal runtime di concorrenza. Si consiglia di non seguire questo modello, poiché potrebbe causare un deadlock 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).
[vai all'inizio]
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.
[vai all'inizio]
Vedere anche
Attività
Procedura: utilizzare Alloc e Free per migliorare le prestazioni di memoria
Procedura: utilizzare l'oversubscription per compensare la latenza
Procedura dettagliata: rimozione di lavoro da un thread dell'interfaccia utente
Concetti
Procedure consigliate del runtime di concorrenza
PPL (Parallel Patterns Library)
Utilità di pianificazione (runtime di concorrenza)
Strutture di dati di sincronizzazione
Confronto delle strutture di dati di sincronizzazione con l'API Windows
Altre risorse
Procedura: utilizzare la classe Context per implementare una classe semaforo di cooperazione
Procedure consigliate nella libreria PPL (Parallel Patterns Library)
Procedure consigliate nella libreria di agenti asincroni
Cronologia delle modifiche
Data |
Cronologia |
Motivo |
---|---|---|
Marzo 2011 |
Sono state aggiunte informazioni sulla possibilità di un deadlock in ambito globale. |
Commenti e suggerimenti dei clienti. |