Condividi tramite


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

  1. Dichiarare una classe denominata semaphore. Aggiungere public sezioni e private a questa classe.
// A semaphore type that uses cooperative blocking semantics.
class semaphore
{
public:
private:
};
  1. private Nella sezione della semaphore 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;
  1. public Nella sezione della semaphore classe implementare il costruttore . Il costruttore accetta un long long valore che specifica il numero massimo di contesti che possono contenere simultaneamente il blocco.
explicit semaphore(long long capacity)
   : _semaphore_count(capacity)
{
}
  1. public Nella sezione della semaphore classe implementare il acquire 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();
   }
}
  1. public Nella sezione della semaphore classe implementare il release 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::UnblockContext::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);
});

Vedi anche

Contesti
Contenitori e oggetti paralleli