Compartir por


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

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

Comentarios

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:
};
  1. En la sección private de la clase semaphore, declare una variable std::atomic que contenga el recuento del semáforo y un objeto concurrency::concurrent_queue que incluya 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;
  1. 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)
{
}
  1. 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 es negativo, agregue el contexto actual al final de la cola de espera y llame al método 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();
   }
}
  1. 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::Block y Context::Yield 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 release usa un bucle de giro que llama al método concurrency::Context::Yield para esperar a que el método 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 wmain usa el algoritmo concurrency::parallel_for para crear varias tareas que necesitan 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 más información sobre la clase concurrent_queue, consulte Contenedores y objetos paralelos. Para 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 en un archivo denominado cooperative-semaphore.cpp y, después, ejecute el siguiente comando en una ventana del símbolo del sistema de Visual Studio.

cl.exe /EHsc cooperative-semaphore.cpp

Programación sólida

Puede usar el patrón 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 scoped_lock se parece a las clases concurrency::critical_section::scoped_lock y 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);
});

Consulte también

Contextos
Contenedores y objetos paralelos