Procedura: utilizzare la classe Context per implementare una classe semaforo di cooperazione
Questo argomento illustra come usare la classe concurrency::Context per implementare una classe semaforo cooperativo.
Osservazioni:
La Context
classe consente di bloccare o restituire il contesto di esecuzione corrente. Il blocco o la resa del contesto corrente è utile quando il contesto corrente non può continuare perché una risorsa non è disponibile. Un semaforo è un esempio di una situazione in cui il contesto di esecuzione corrente deve attendere che una risorsa diventi disponibile. Un semaforo, come un oggetto sezione critica, è un oggetto di sincronizzazione che consente al codice in un contesto di avere accesso esclusivo a una risorsa. Tuttavia, a differenza di un oggetto sezione critica, un semaforo consente a più contesti di accedere simultaneamente alla risorsa. Se il numero massimo di contesti contiene un blocco semaforo, ogni contesto aggiuntivo deve attendere che un altro contesto rilasci il blocco.
Per implementare la classe semaforo
- Dichiarare una classe denominata
semaphore
. Aggiungerepublic
sezioni eprivate
a questa classe.
// A semaphore type that uses cooperative blocking semantics.
class semaphore
{
public:
private:
};
private
Nella sezione dellasemaphore
classe dichiarare una variabile std::atomic che contiene il conteggio dei semafori e un oggetto concurrency::concurrent_queue che contiene i contesti che devono attendere l'acquisizione del semaforo.
// The semaphore count.
atomic<long long> _semaphore_count;
// A concurrency-safe queue of contexts that must wait to
// acquire the semaphore.
concurrent_queue<Context*> _waiting_contexts;
public
Nella sezione dellasemaphore
classe implementare il costruttore . Il costruttore accetta unlong long
valore che specifica il numero massimo di contesti che possono contenere simultaneamente il blocco.
explicit semaphore(long long capacity)
: _semaphore_count(capacity)
{
}
public
Nella sezione dellasemaphore
classe implementare ilacquire
metodo . Questo metodo decrementa il conteggio del semaforo come operazione atomica. Se il conteggio semaforo diventa negativo, aggiungere il contesto corrente alla fine della coda di attesa e chiamare il metodo concurrency::Context::Block per bloccare il contesto corrente.
// Acquires access to the semaphore.
void acquire()
{
// The capacity of the semaphore is exceeded when the semaphore count
// falls below zero. When this happens, add the current context to the
// back of the wait queue and block the current context.
if (--_semaphore_count < 0)
{
_waiting_contexts.push(Context::CurrentContext());
Context::Block();
}
}
public
Nella sezione dellasemaphore
classe implementare ilrelease
metodo . Questo metodo incrementa il conteggio del semaforo come operazione atomica. Se il numero di semafori è negativo prima dell'operazione di incremento, è presente almeno un contesto in attesa del blocco. In questo caso, sbloccare il contesto che si trova all'inizio della coda di attesa.
// Releases access to the semaphore.
void release()
{
// If the semaphore count is negative, unblock the first waiting context.
if (++_semaphore_count <= 0)
{
// A call to acquire might have decremented the counter, but has not
// yet finished adding the context to the queue.
// Create a spin loop that waits for the context to become available.
Context* waiting = NULL;
while (!_waiting_contexts.try_pop(waiting))
{
Context::Yield();
}
// Unblock the context.
waiting->Unblock();
}
}
Esempio
La semaphore
classe in questo esempio si comporta in modo cooperativo perché i Context::Block
metodi e Context::Yield
producono l'esecuzione in modo che il runtime possa eseguire altre attività.
Il acquire
metodo decrementa il contatore, ma potrebbe non terminare l'aggiunta del contesto alla coda di attesa prima che un altro contesto chiami il release
metodo . Per tenere conto di questo problema, il release
metodo usa un ciclo spin che chiama il metodo concurrency::Context::Yield per attendere che il acquire
metodo finisca di aggiungere il contesto.
Il release
metodo può chiamare il Context::Unblock
metodo prima che il acquire
metodo chiami il Context::Block
metodo . Non è necessario proteggersi da questa race condition perché il runtime consente di chiamare questi metodi in qualsiasi ordine. Se il metodo chiama prima che release
il acquire
metodo chiami Context::Unblock
Context::Block
lo stesso contesto, tale contesto rimane sbloccato. Il runtime richiede solo che ogni chiamata a Context::Block
corrisponda a una chiamata corrispondente a Context::Unblock
.
Nell'esempio seguente viene illustrata la classe completa semaphore
. La wmain
funzione mostra l'utilizzo di base di questa classe. La wmain
funzione usa l'algoritmo concurrency::p arallel_for per creare diverse attività che richiedono l'accesso al semaforo. Poiché tre thread possono contenere il blocco in qualsiasi momento, alcune attività devono attendere il completamento e il rilascio del blocco da parte di un'altra attività.
// cooperative-semaphore.cpp
// compile with: /EHsc
#include <atomic>
#include <concrt.h>
#include <ppl.h>
#include <concurrent_queue.h>
#include <iostream>
#include <sstream>
using namespace concurrency;
using namespace std;
// A semaphore type that uses cooperative blocking semantics.
class semaphore
{
public:
explicit semaphore(long long capacity)
: _semaphore_count(capacity)
{
}
// Acquires access to the semaphore.
void acquire()
{
// The capacity of the semaphore is exceeded when the semaphore count
// falls below zero. When this happens, add the current context to the
// back of the wait queue and block the current context.
if (--_semaphore_count < 0)
{
_waiting_contexts.push(Context::CurrentContext());
Context::Block();
}
}
// Releases access to the semaphore.
void release()
{
// If the semaphore count is negative, unblock the first waiting context.
if (++_semaphore_count <= 0)
{
// A call to acquire might have decremented the counter, but has not
// yet finished adding the context to the queue.
// Create a spin loop that waits for the context to become available.
Context* waiting = NULL;
while (!_waiting_contexts.try_pop(waiting))
{
Context::Yield();
}
// Unblock the context.
waiting->Unblock();
}
}
private:
// The semaphore count.
atomic<long long> _semaphore_count;
// A concurrency-safe queue of contexts that must wait to
// acquire the semaphore.
concurrent_queue<Context*> _waiting_contexts;
};
int wmain()
{
// Create a semaphore that allows at most three threads to
// hold the lock.
semaphore s(3);
parallel_for(0, 10, [&](int i) {
// Acquire the lock.
s.acquire();
// Print a message to the console.
wstringstream ss;
ss << L"In loop iteration " << i << L"..." << endl;
wcout << ss.str();
// Simulate work by waiting for two seconds.
wait(2000);
// Release the lock.
s.release();
});
}
In questo esempio viene generato l'output di esempio seguente.
In loop iteration 5...
In loop iteration 0...
In loop iteration 6...
In loop iteration 1...
In loop iteration 2...
In loop iteration 7...
In loop iteration 3...
In loop iteration 8...
In loop iteration 9...
In loop iteration 4...
Per altre informazioni sulla concurrent_queue
classe , vedere Contenitori e oggetti paralleli. Per altre informazioni sull'algoritmo parallel_for
, vedere Algoritmi paralleli.
Compilazione del codice
Copiare il codice di esempio e incollarlo in un progetto di Visual Studio oppure incollarlo in un file denominato cooperative-semaphore.cpp
e quindi eseguire il comando seguente in una finestra del prompt dei comandi di Visual Studio.
cl.exe /EHsc cooperative-semaphore.cpp
Programmazione efficiente
È possibile usare il modello di inizializzazione delle risorse (RAII) per limitare l'accesso a un semaphore
oggetto a 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. Di conseguenza, la risorsa viene gestita correttamente quando viene generata un'eccezione o quando una funzione contiene più return
istruzioni.
Nell'esempio seguente viene definita una classe denominata scoped_lock
, definita nella public
sezione della semaphore
classe . La scoped_lock
classe è simile alle classi concurrency::critical_section::scoped_lock e concurrency::reader_writer_lock::scoped_lock . Il costruttore della semaphore::scoped_lock
classe acquisisce l'accesso all'oggetto specificato semaphore
e il distruttore rilascia l'accesso a tale oggetto.
// An exception-safe RAII wrapper for the semaphore class.
class scoped_lock
{
public:
// Acquires access to the semaphore.
scoped_lock(semaphore& s)
: _s(s)
{
_s.acquire();
}
// Releases access to the semaphore.
~scoped_lock()
{
_s.release();
}
private:
semaphore& _s;
};
Nell'esempio seguente viene modificato il corpo della funzione di lavoro passata all'algoritmo parallel_for
in modo che usi RAII per assicurarsi che il semaforo venga rilasciato prima che la funzione restituisca. Questa tecnica garantisce che la funzione di lavoro sia indipendente dalle eccezioni.
parallel_for(0, 10, [&](int i) {
// Create an exception-safe scoped_lock object that holds the lock
// for the duration of the current scope.
semaphore::scoped_lock auto_lock(s);
// Print a message to the console.
wstringstream ss;
ss << L"In loop iteration " << i << L"..." << endl;
wcout << ss.str();
// Simulate work by waiting for two seconds.
wait(2000);
});