Compartir a través de


Cómo: Usar la clase Context para implementar un semáforo cooperativo

En este tema se muestra cómo utilizar la clase de concurrency::Context para implementar una clase de semáforo cooperativa.

La clase Context permite bloquear o ceder el contexto de ejecución actual.Bloquear o ceder el contexto actual es útil cuando el contexto actual no puede continuar porque un recurso no está disponible.Un semáforo es un ejemplo de una situación en la que el contexto de ejecución actual debe esperar a que un recurso esté disponible.Un semáforo, como un objeto de sección crítica, es un objeto de sincronización que permite que el código en un contexto tenga acceso exclusivo a un recurso.Sin embargo, a diferencia de lo que sucede con un objeto de sección crítica, un semáforo permite que varios contextos tengan acceso al recurso a la vez.Si existe un bloqueo de semáforo para un número máximo de contextos, cada contexto adicional debe esperar a que el otro contexto libere el bloqueo.

Para implementar la clase de semáforo

  1. Declare una clase que se denomine semaphore.Agregue secciones public y private a esta clase.

    // A semaphore type that uses cooperative blocking semantics.
    class semaphore
    {
    public:
    private:
    };
    
  2. En la sección de private de la clase de semaphore , declare una variable de std::atomic que contiene el recuento del semáforo y un objeto de concurrency::concurrent_queue que contiene los contextos que deben esperar para adquirir el semáforo.

    // 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. En la sección public de la clase semaphore, implemente el constructor.El constructor toma un valor long long que especifica el número máximo de contextos que pueden mantener el bloqueo al mismo tiempo.

    explicit semaphore(long long capacity)
       : _semaphore_count(capacity)
    {
    }
    
  4. En la sección public de la clase semaphore, implemente el método acquire.Este método disminuye el recuento del semáforo como una operación atómica.Si el recuento del semáforo se vuelve negativo, agregue el contexto actual al final de la cola de espera y llame al método de concurrency::Context::Block para bloquear el contexto actual.

    // 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. En la sección public de la clase semaphore, implemente el método release.Este método aumenta el recuento del semáforo como una operación atómica.Si el recuento del semáforo es negativo antes de la operación de incremento, hay por lo menos un contexto que está esperando por el bloqueo.En ese caso, desbloquee el contexto que está al principio de la cola de espera.

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

Ejemplo

La clase semaphore de este ejemplo se comporta de manera cooperativa, porque los métodos Context::Yield y Context::Block ceden la ejecución de modo que el runtime pueda realizar otras tareas.

El método acquire disminuye el contador, pero es posible que no termine de agregar el contexto a la cola de espera antes de que otro contexto llame al método release.Para solucionarlo, el método de release usa un bucle de giro que llame al método de concurrency::Context::Yield para esperar a que el método de acquire termine de agregar el contexto.

El método release puede llamar a Context::Unblock antes de que el método acquire llame a Context::Block.No es necesario evitar esta condición de carrera porque el runtime permite llamar a estos métodos en cualquier orden.Si el método release llama a Context::Unblock antes de que el método acquire llame a Context::Block para el mismo contexto, ese contexto permanece desbloqueado.El runtime sólo requiere que cada llamada a Context::Block se corresponda con la respectiva llamada a Context::Unblock.

En el ejemplo siguiente se muestra la clase semaphore completa:La función wmain muestra el uso básico de esta clase.La función de wmain utiliza el algoritmo de concurrency::parallel_for para crear varias tareas que requieren acceso al semáforo.Dado que puede haber tres subprocesos que mantengan el bloqueo en un momento dado, algunas tareas deben esperar a que otra tarea finalice y libere el bloqueo.

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

Este ejemplo genera la siguiente salida de ejemplo.

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...

Para obtener más información sobre la clase concurrent_queue, vea Contenedores y objetos paralelos.Para obtener más información sobre el algoritmo parallel_for, vea Algoritmos paralelos.

Compilar el código

Copie el código de ejemplo y péguelo en un proyecto de Visual Studio, o péguelo en un archivo denominado cooperative-semaphore.cpp y después se ejecute el siguiente comando en una ventana de símbolo del sistema de Visual Studio.

cl.exe /EHsc cooperative-semaphore.cpp

Programación eficaz

Puede usar el modelo Resource Acquisition Is Initialization (RAII) para limitar el acceso a un objeto semaphore a un ámbito determinado.Bajo el modelo RAII, se asigna una estructura de datos en la pila.Esa estructura de datos se inicializa o adquiere un recurso cuando se crea, y destruye o libera ese recurso cuando se destruye la estructura de datos.El modelo RAII garantiza que se llama al destructor antes de que el ámbito de inclusión salga.Por consiguiente, se administra el recurso correctamente cuando se produce una excepción o cuando una función contiene varias instrucciones return.

En el siguiente ejemplo se define una clase denominada scoped_lock, que se define en la sección public de la clase semaphore.La clase de scoped_lock es similar a las clases de concurrency::critical_section::scoped_lock y de concurrency::reader_writer_lock::scoped_lock .El constructor de la clase semaphore::scoped_lock adquiere el acceso al objeto semaphore en cuestión y el destructor libera el acceso a ese objeto.

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

En el ejemplo siguiente se modifica el cuerpo de la función de trabajo que se pasa al algoritmo parallel_for para que use RAII con el fin de garantizar que el semáforo se libere antes de que regrese la función.Esta técnica garantiza que la función de trabajo es segura ante excepciones.

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

Vea también

Referencia

Context (Clase)

Conceptos

Contextos

Contenedores y objetos paralelos