Partilhar via


Como: Usar a classe de contexto para implementar um semáforo cooperativo

Este tópico mostra como usar a classe concurrency::Context para implementar uma classe semáforo cooperativa.

Observações

A classe Context permite bloquear ou suspender o contexto de execução atual. Bloquear ou ceder o contexto atual é útil quando o contexto atual não pode prosseguir porque um recurso não está disponível. Um semáforo é um exemplo de uma situação em que o contexto de execução atual deve esperar que um recurso fique disponível. Um semáforo, como um objeto de seção crítica, é um objeto de sincronização que permite que o código em um contexto tenha acesso exclusivo a um recurso. No entanto, ao contrário de um objeto de seção crítica, um semáforo permite que mais de um contexto acesse o recurso simultaneamente. Se o número máximo de contextos mantiver um bloqueio de semáforo, cada contexto adicional deverá aguardar que outro contexto libere o bloqueio.

Para implementar a classe semáforo

  1. Declare uma classe chamada semaphore. Adicione public e private seções a esta classe.
// A semaphore type that uses cooperative blocking semantics.
class semaphore
{
public:
private:
};
  1. Na secção private da classe semaphore, declare uma variável std::atomic que mantém a contagem do semáforo e um objeto concurrency::concurrent_queue que mantém os contextos que devem esperar para adquirir o 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. Na classe public, na seção semaphore, implemente o construtor. O construtor usa um long long valor que especifica o número máximo de contextos que podem conter simultaneamente o bloqueio.
explicit semaphore(long long capacity)
   : _semaphore_count(capacity)
{
}
  1. public Na seção da semaphore classe, implemente o acquire método. Este método diminui a contagem de semáforos como uma operação atómica. Se a contagem de semáforos se tornar negativa, adicione o contexto atual ao final da fila de espera e chame o método concurrency::Context::Block para bloquear o contexto atual.
// 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 Na seção da semaphore classe, implemente o release método. Este método incrementa a contagem de semáforos como uma operação atómica. Se a contagem de semáforos for negativa antes da operação de incremento, há pelo menos um contexto que está aguardando o bloqueio. Nesse caso, desbloqueie o contexto que está na frente da fila 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();
   }
}

Exemplo

A semaphore classe neste exemplo se comporta cooperativamente porque os Context::Block métodos e Context::Yield produzem execução para que o tempo de execução possa executar outras tarefas.

O acquire método diminui o contador, mas pode não terminar de adicionar o contexto à fila de espera antes que outro contexto chame o release método. Para levar isso em conta, o release método usa um loop de rotação que chama o método concurrency::Context::Yield para aguardar que o acquire método termine de adicionar o contexto.

O release método pode chamar o Context::Unblock método antes que o método chame acquire o Context::Block método. Você não precisa se proteger contra essa condição de corrida porque o tempo de execução permite que esses métodos sejam chamados em qualquer ordem. Se o release método chama Context::Unblock antes do acquire método chama Context::Block para o mesmo contexto, esse contexto permanece desbloqueado. O tempo de execução requer apenas que cada chamada para Context::Block seja correspondida com uma chamada correspondente para Context::Unblock.

O exemplo a seguir mostra a classe completa semaphore . A wmain função mostra o uso básico desta classe. A wmain função usa o algoritmo concurrency::parallel_for para criar várias tarefas que exigem acesso ao semáforo. Como três threads podem manter o bloqueio a qualquer momento, algumas tarefas devem aguardar a conclusão de outra tarefa e liberar o bloqueio.

// 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 exemplo gera a seguinte saída.

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 obter mais informações sobre a concurrent_queue classe, consulte Contêineres e objetos paralelos. Para obter mais informações sobre o parallel_for algoritmo, consulte Algoritmos paralelos.

Compilando o código

Copie o código de exemplo e cole-o em um projeto do Visual Studio ou cole-o em um arquivo chamado cooperative-semaphore.cpp e, em seguida, execute o seguinte comando em uma janela do prompt de comando do Visual Studio.

cl.exe /EHsc cooperative-semaphore.cpp

Programação robusta

Você pode usar o padrão RAII (Resource Acquisition Is Initialization ) para limitar o acesso a um semaphore objeto a um determinado escopo. Sob o padrão RAII, uma estrutura de dados é alocada na pilha. Essa estrutura de dados inicializa ou adquire um recurso quando ele é criado e destrói ou libera esse recurso quando a estrutura de dados é destruída. O padrão RAII garante que o destruidor seja chamado antes que o escopo de fechamento saia. Portanto, o recurso é gerenciado corretamente quando uma exceção é lançada ou quando uma função contém várias return instruções.

O exemplo a seguir define uma classe chamada scoped_lock, que é definida na public seção da semaphore classe. A scoped_lock classe é semelhante às classes concorrência::critical_section::scoped_lock e concorrência::reader_writer_lock::scoped_lock. O construtor da semaphore::scoped_lock classe adquire acesso ao objeto dado semaphore e o destruidor libera o acesso a esse 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;
};

O exemplo a seguir modifica o corpo da função de trabalho que é passada para o parallel_for algoritmo para que ele use RAII para garantir que o semáforo seja liberado antes que a função retorne. Esta técnica garante que a função de trabalho é excecionalmente segura.

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

Ver também

Contextos
Contêineres e objetos paralelos