Comment : utiliser la classe Context pour implémenter un sémaphore coopératif
Cette rubrique indique comment utiliser la classe concurrency::Context pour implémenter une classe de sémaphore coopérative.
La classe Context vous permet de bloquer ou de céder le contexte d'exécution actuel. Le blocage ou la cession 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 dans laquelle le contexte d'exécution actuel doit attendre qu'une ressource devienne disponible. Un sémaphore, comme un objet de section critique, est un objet de synchronisation qui permet au code dans un contexte de disposer d'un accès exclusif à une ressource. Cependant, contrairement à un objet de section critique, un sémaphore permet à plusieurs contextes d'accéder simultanément à la ressource. Si la quantité maximale de contextes détient un verrou de sémaphore, chaque contexte supplémentaire doit attendre qu'un autre contexte libère le verrou.
Pour implémenter la classe de sémaphore
Déclarez une classe nommée semaphore. Ajoutez des sections public et private à cette classe.
// A semaphore type that uses cooperative blocking semantics. class semaphore { public: private: };
Dans la section private de la classe semaphore, déclarez une variable de type hh874651(v=vs.120).mdqui contient le nombre de sémaphores et un objetconcurrency::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;
Dans la section public de la classe semaphore, implémentez le constructeur. Le constructeur prend une valeur long long qui spécifie le nombre maximal de contextes qui peuvent détenir simultanément le verrou.
explicit semaphore(long long capacity) : _semaphore_count(capacity) { }
Dans la section public de la classe semaphore, implémentez la méthode acquire. 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(); } }
Dans la section public de la classe semaphore, implémentez la méthode release. 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ément, il y a au moins un contexte qui attend le verrou. Dans ce cas, débloquez le contexte qui est en tête 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 classe semaphore dans cet exemple se comporte de manière coopérative car les méthodes Context::Block et Context::Yield cèdent l'exécution de sorte que le runtime puisse effectuer d'autres tâches.
La méthode acquire décrémente le compteur, mais elle risque de ne pas avoir fini d'ajouter le contexte à la file d'attente avant qu'un autre contexte appelle la méthode release. Pou remédier à cela, la méthode release utilise une boucle de rotation qui appelle la méthode concurrency::Context::Yield pour attendre que la méthode acquire ait terminé d'ajouter le contexte.
La méthode release peut appeler la méthode Context::Unblock avant que la méthode acquire n'appelle la méthode Context::Block. Vous n'êtes pas obligé de vous protéger contre cette condition de concurrence car le runtime autorise l'appel de ces méthodes dans n'importe quel ordre. Si la méthode release appelle Context::Unblock avant que la méthode acquire n'appelle Context::Block pour le même contexte, ce contexte reste non bloqué. Le runtime requiert seulement que pour chaque appel à Context::Block corresponde un appel à Context::Unblock.
L'exemple suivant illustre la classe semaphore complète. La fonction wmain illustre l'utilisation de base de cette classe. La fonction wmain utilise l'algorithme concurrency::parallel_for pour créer plusieurs tâches qui requièrent l'accès au sémaphore. Étant donné que trois threads peuvent détenir 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.
Pour plus d'informations sur la classe concurrent_queue, 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 RAII (Resource Acquisition Is Initialization) pour limiter l'accès à un objet semaphore à une portée donnée. Selon 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 que la portée englobante ne quitte. Par conséquent, la ressource est gérée correctement lorsqu'une exception est levée ou lorsqu'une fonction contient plusieurs instructions return.
L'exemple suivant définit une classe nommée scoped_lock, définie dans la section public de la classe semaphore. La classe scoped_lock s'apparente aux classes concurrency::critical_section::scoped_lock et concurrency::reader_writer_lock::scoped_lock. Le constructeur de la classe semaphore::scoped_lock acquiert l'accès à l'objet semaphore donné 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 de sorte qu'elle utilise le modèle RAII pour s'assurer que le sémaphore est libéré avant le retour de la fonction. Cette technique permet de s'assurer que la fonction de travail est sécurisée du point de vue des exceptions.
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);
});