Condividi tramite


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

In questo argomento viene illustrato come utilizzare la classe concurrency::Context per implementare una classe semaforo di cooperazione.

La classe Context consente di bloccare o restituire il contesto di esecuzione corrente.Il blocco o la restituzione del contesto corrente è utile quando il contesto corrente non può procedere poiché 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, simile a un oggetto sezione critica, è un oggetto di sincronizzazione che consente al codice di un contesto di avere l'accesso esclusivo a una risorsa.Tuttavia, a differenza di un oggetto sezione critica, un semaforo consente a più contesti di accedere alla risorsa contemporaneamente.Se il numero massimo di contesti contiene un blocco del 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 a questa classe le sezioni public e private.

    // A semaphore type that uses cooperative blocking semantics.
    class semaphore
    {
    public:
    private:
    };
    
  2. Nella sezione private della classe semaphore, dichiarare una variabile std::atomic che utilizza il conteggio del semaforo e un oggetto concurrency::concurrent_queue contenente 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;
    
  3. Nella sezione public della classe semaphore implementare il costruttore.Il costruttore accetta un valore long long che specifica il numero massimo di contesti che possono contenere contemporaneamente il blocco.

    explicit semaphore(long long capacity)
       : _semaphore_count(capacity)
    {
    }
    
  4. Nella sezione public della classe semaphore implementare il metodo acquire.Questo metodo decrementa il conteggio del semaforo come operazione atomica.Se il conteggio del 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();
       }
    }
    
  5. Nella sezione public della classe semaphore implementare il metodo release.Questo metodo incrementa il conteggio del semaforo come operazione atomica.Se il conteggio del semaforo è 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 classe semaphore di questo esempio si comporta in modo cooperativo poiché i metodi Context::Block e Context::Yield restituiscono l'esecuzione in modo tale che il runtime possa eseguire le altre attività.

Il metodo acquire decrementa il contatore, ma potrebbe non completare l'aggiunta del contesto alla coda di attesa prima che un altro contesto chiami il metodo release.Per risolvere questa situazione, il metodo release utilizza un ciclo di attesa che chiama il metodo concurrency::Context::Yield per attendere il metodo acquire per completare l'aggiunta del contesto.

Il metodo release può chiamare il metodo Context::Unblock prima che il metodo acquire chiami il metodo Context::Block.Non è necessario effettuare alcuna operazione per evitare che si verifichi tale race condition poiché il runtime consente la chiamata a questi metodi in qualsiasi ordine.Se il metodo release chiama Context::Unblock prima che il metodo acquire chiami Context::Block per lo stesso contesto, tale contesto rimarrà sbloccato.Il runtime richiede solo che a ogni chiamata a Context::Block venga associata una chiamata corrispondente a Context::Unblock.

Nell'esempio seguente viene illustrata la classe semaphore completa.La funzione wmain mostra l'utilizzo di base di questa classe.La funzione wmain utilizza l'algoritmo concurrency::parallel_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 di un'altra attività e il rilascio del blocco.

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

Questo esempio produce l'output 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 ulteriori informazioni sulla classe concurrent_queue, vedere Contenitori e oggetti paralleli.Per ulteriori 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 quindi viene eseguito il comando seguente in una finestra del prompt dei comandi di Visual Studio.

cl.exe /EHsc cooperative-semaphore.cpp

Programmazione efficiente

È possibile utilizzare il modello RAII (Resource Acquisition Is Initialization) per consentire a un oggetto semaphore di accedere solo a un ambito specificato.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.Pertanto, la risorsa viene gestita correttamente quando viene generata un'eccezione o quando una funzione contiene più istruzioni return.

Nell'esempio seguente viene definita una classe denominata scoped_lock, definita nella sezione public della classe semaphore.La classe scoped_lock assomiglia alle classi concurrency::reader_writer_lock::scoped_lock e concurrency::critical_section::scoped_lock.Il costruttore della classe semaphore::scoped_lock acquisisce l'accesso all'oggetto semaphore specificato 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 lavoro passata all'algoritmo parallel_for in modo da utilizzare il modello RAII per assicurarsi che il semaforo venga rilasciato prima del termine della funzione.Questa tecnica garantisce che la funzione lavoro venga eseguita correttamente indipendentemente 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);
});

Vedere anche

Riferimenti

Classe Context

Concetti

Contesti

Contenitori e oggetti paralleli