Comment : utiliser la classe Context pour implémenter un sémaphore coopératif

Cette rubrique montre comment utiliser la classe concurrency ::Context pour implémenter une classe sémaphore coopérative.

Notes

La Context classe vous permet de bloquer ou de générer le contexte d’exécution actuel. Le blocage ou le rendement du contexte actuel est utile lorsque le contexte actuel ne peut pas continuer, car une ressource n’est pas disponible. Un sémaphore est un exemple de situation où le contexte d’exécution actuel doit attendre qu’une ressource soit disponible. Un sémaphore, comme un objet de section critique, est un objet de synchronisation qui permet au code dans un contexte d’avoir un accès exclusif à une ressource. Toutefois, contrairement à un objet de section critique, un sémaphore permet à plusieurs contextes d’accéder simultanément à la ressource. Si le nombre maximal de contextes contient un verrou sémaphore, chaque contexte supplémentaire doit attendre qu’un autre contexte libère le verrou.

Pour implémenter la classe sémaphore

  1. Déclarez une classe nommée semaphore. Ajoutez public et private sections à cette classe.
// A semaphore type that uses cooperative blocking semantics.
class semaphore
{
public:
private:
};
  1. Dans la private section de la semaphore classe, déclarez une variable std ::atomic qui contient le nombre de sémaphores et un objet concurrency ::concurrent_queue qui contient les contextes qui doivent attendre pour acquérir le sémaphore.
// 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. Dans la public section de la semaphore classe, implémentez le constructeur. Le constructeur prend une long long valeur qui spécifie le nombre maximal de contextes pouvant contenir simultanément le verrou.
explicit semaphore(long long capacity)
   : _semaphore_count(capacity)
{
}
  1. Dans la public section de la semaphore classe, implémentez la acquire méthode. Cette méthode décrémente le nombre de sémaphores en tant qu’opération atomique. Si le nombre de sémaphores devient négatif, ajoutez le contexte actuel à la fin de la file d’attente et appelez la méthode concurrency ::Context ::Block pour bloquer le contexte actuel.
// 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. Dans la public section de la semaphore classe, implémentez la release méthode. Cette méthode incrémente le nombre de sémaphores en tant qu’opération atomique. Si le nombre de sémaphores est négatif avant l’opération d’incrémentation, il existe au moins un contexte qui attend le verrou. Dans ce cas, débloquez le contexte qui se trouve à l’avant de la file d’attente.
// 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();
   }
}

Exemple

La semaphore classe de cet exemple se comporte de manière coopérative, car les Context::Block Context::Yield méthodes produisent l’exécution afin que le runtime puisse effectuer d’autres tâches.

La acquire méthode décrémente le compteur, mais elle risque de ne pas terminer l’ajout du contexte à la file d’attente avant qu’un autre contexte appelle la release méthode. Pour ce faire, la release méthode utilise une boucle de rotation qui appelle la méthode concurrency ::Context ::Yield pour attendre que la acquire méthode termine l’ajout du contexte.

La release méthode peut appeler la Context::Unblock méthode avant que la acquire méthode appelle la Context::Block méthode. Vous n’avez pas besoin de vous protéger contre cette condition de concurrence, car le runtime permet d’appeler ces méthodes dans un ordre quelconque. Si la release méthode appelle Context::Unblock avant l’appel Context::Block de la acquire méthode pour le même contexte, ce contexte reste déblocé. Le runtime nécessite uniquement que chaque appel soit Context::Block mis en correspondance avec un appel correspondant à Context::Unblock.

L’exemple suivant montre la classe complète semaphore . La fonction affiche l’utilisation wmain de base de cette classe. La wmain fonction utilise l’algorithme concurrency ::p arallel_for pour créer plusieurs tâches qui nécessitent l’accès au sémaphore. Étant donné que trois threads peuvent contenir le verrou à tout moment, certaines tâches doivent attendre qu’une autre tâche se termine et libère le verrou.

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

Cet exemple génère l’exemple de sortie suivant.

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

Pour plus d’informations sur la concurrent_queue classe, consultez Conteneurs et objets parallèles. Pour plus d’informations sur l’algorithme parallel_for , consultez Algorithmes parallèles.

Compilation du code

Copiez l’exemple de code et collez-le dans un projet Visual Studio, ou collez-le dans un fichier nommé cooperative-semaphore.cpp , puis exécutez la commande suivante dans une fenêtre d’invite de commandes Visual Studio.

cl.exe /EHsc cooperative-semaphore.cpp

Programmation fiable

Vous pouvez utiliser le modèle d’initialisation d’acquisition de ressources (RAII) pour limiter l’accès à un semaphore objet à une étendue donnée. Sous le modèle RAII, une structure de données est allouée sur la pile. Cette structure de données initialise ou acquiert une ressource lorsqu’elle est créée et détruit ou libère cette ressource lorsque la structure de données est détruite. Le modèle RAII garantit que le destructeur est appelé avant la sortie de l’étendue englobante. Par conséquent, la ressource est correctement gérée lorsqu’une exception est levée ou lorsqu’une fonction contient plusieurs return instructions.

L’exemple suivant définit une classe nommée scoped_lock, qui est définie dans la public section de la semaphore classe. La scoped_lock classe ressemble aux classes concurrency ::critical_section ::scoped_lock et concurrency ::reader_writer_lock ::scoped_lock . Le constructeur de la semaphore::scoped_lock classe acquiert l’accès à l’objet donné semaphore et le destructeur libère l’accès à cet objet.

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

L’exemple suivant modifie le corps de la fonction de travail passée à l’algorithme parallel_for afin qu’il utilise RAII pour s’assurer que le sémaphore est libéré avant que la fonction ne retourne. Cette technique garantit que la fonction de travail est sans risque d’exception.

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

Voir aussi

Contextes
Conteneurs et objets parallèles