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
- Déclarez une classe nommée
semaphore
. Ajoutezpublic
etprivate
sections à cette classe.
// A semaphore type that uses cooperative blocking semantics.
class semaphore
{
public:
private:
};
- Dans la
private
section de lasemaphore
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;
- Dans la
public
section de lasemaphore
classe, implémentez le constructeur. Le constructeur prend unelong long
valeur qui spécifie le nombre maximal de contextes pouvant contenir simultanément le verrou.
explicit semaphore(long long capacity)
: _semaphore_count(capacity)
{
}
- Dans la
public
section de lasemaphore
classe, implémentez laacquire
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();
}
}
- Dans la
public
section de lasemaphore
classe, implémentez larelease
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);
});