Quand faut-il utiliser un sémaphore et quand utiliser une variable conditionnelle (CondVar)?
Les serrures sont utilisées pour l'exclusion mutuelle. Lorsque vous voulez vous assurer qu'un morceau de code est atomique, verrouillez-le. Vous pouvez théoriquement utiliser un sémaphore binaire pour le faire, mais c'est un cas spécial.
Les sémaphores et les variables de condition reposent sur l'exclusion mutuelle fournie par les verrous et sont utilisés pour fournir un accès synchronisé aux ressources partagées. Ils peuvent être utilisés à des fins similaires.
Une variable de condition est généralement utilisée pour éviter l'attente occupée (bouclage répété tout en vérifiant une condition) en attendant qu'une ressource soit disponible. Par exemple, si vous avez un thread (ou plusieurs threads) qui ne peut pas continuer tant qu'une file d'attente n'est pas vide, l'approche en attente avec occupation serait simplement de faire quelque chose comme:
//pseudocode
while(!queue.empty())
{
sleep(1);
}
Le problème avec ceci est que vous perdez du temps processeur en demandant à ce fil de vérifier la condition de manière répétée. Pourquoi ne pas plutôt avoir une variable de synchronisation qui peut être signalée pour indiquer au thread que la ressource est disponible?
//pseudocode
syncVar.lock.acquire();
while(!queue.empty())
{
syncVar.wait();
}
//do stuff with queue
syncVar.lock.release();
Vraisemblablement, vous aurez un fil quelque part qui tire des choses de la file d'attente. Lorsque la file est vide, il peut appeler syncVar.signal()
pour réveiller un thread aléatoire endormi sur syncVar.wait()
(ou il y a généralement aussi signalAll()
ou broadcast()
méthode pour réveiller tous les threads en attente).
J'utilise généralement des variables de synchronisation comme celle-ci lorsqu'un ou plusieurs threads attendent une condition particulière (par exemple, la file d'attente doit être vide).
Les sémaphores peuvent être utilisés de la même manière, mais je pense qu’ils sont mieux utilisés quand vous avez une ressource partagée qui peut être disponible et indisponible en fonction d’un nombre entier de choses disponibles. Les sémaphores conviennent aux situations de producteurs/consommateurs, où les producteurs allouent des ressources et les consommateurs les consomment.
Pensez si vous aviez un distributeur automatique de soda. Il n'y a qu'une seule machine à soda et c'est une ressource partagée. Vous avez un fil qui est un vendeur (producteur) qui est responsable de garder la machine en stock et N fils qui sont des acheteurs (consommateurs) qui veulent obtenir des sodas de la machine. Le nombre de sodas dans la machine est la valeur entière qui dirigera notre sémaphore.
Chaque fil d'acheteur (consommateur) qui arrive à la machine à soda appelle la méthode sémaphore down()
pour prendre un soda. Cela va prendre un soda de la machine et décrémenter le nombre de sodas disponibles de 1. S'il existe des sodas disponibles, le code continuera à courir après l'instruction down()
sans problème. Si aucun soda n'est disponible, le fil de discussion dormira ici en attendant d'être averti du moment où le soda sera à nouveau disponible (lorsqu'il y a plus de sodas dans la machine).
Le fil vendeur (producteur) attendrait essentiellement que la machine à soda soit vide. Le fournisseur est averti lorsque le dernier soda est sorti de la machine (et un ou plusieurs consommateurs attendent potentiellement de sortir des sodas). Le fournisseur réapprovisionnerait la machine à soda avec la méthode sémaphore up()
, le nombre de sodas disponibles serait incrémenté à chaque fois et les threads consommateurs en attente seraient ainsi avertis que davantage de soda était disponible.
Les méthodes wait()
et signal()
d'une variable de synchronisation ont tendance à être masquées dans les opérations down()
et up()
du sémaphore.
Certes, il y a un chevauchement entre les deux choix. Il existe de nombreux scénarios dans lesquels un sémaphore ou une variable de condition (ou un ensemble de variables de condition) peuvent tous deux vous servir. Les sémaphores et les variables de condition sont associés à un objet de verrouillage qu'ils utilisent pour maintenir l'exclusion mutuelle, mais ils fournissent ensuite des fonctionnalités supplémentaires au-dessus du verrou pour la synchronisation de l'exécution du thread. C’est surtout à vous de déterminer laquelle est la mieux adaptée à votre situation.
Ce n’est pas nécessairement la description la plus technique, mais c’est ce qui me semble logique.
Voyons ce qu'il y a sous le capot.
La variable conditionnelle est essentiellement une file d'attente, qui prend en charge les opérations de blocage-attente et de réveil, c'est-à-dire que vous pouvez mettre un thread dans la file d'attente et définir son état sur BLOCK et en extraire un thread. et mettre son état à READY.
Notez que pour utiliser une variable conditionnelle, deux autres éléments sont nécessaires:
Le protocole devient alors,
Semaphore est essentiellement un compteur + un mutex + une file d'attente. Et il peut être utilisé tel quel sans dépendances externes. Vous pouvez l'utiliser comme un mutex ou comme une variable conditionnelle.
Par conséquent, le sémaphore peut être traité comme une structure plus sophistiquée que la variable conditionnelle, alors que cette dernière est plus légère et flexible.
Les sémaphores peuvent être utilisés pour implémenter un accès exclusif aux variables, mais ils sont destinés à être utilisés pour la synchronisation. Les mutex, par contre, ont une sémantique strictement liée à l'exclusion mutuelle: seul le processus qui a verrouillé la ressource est autorisé à la déverrouiller.
Malheureusement, vous ne pouvez pas implémenter la synchronisation avec les mutex, c'est pourquoi nous avons des variables de condition. Notez également qu'avec les variables de condition, vous pouvez déverrouiller tous les threads en attente au même instant en utilisant le déverrouillage de la diffusion. Cela ne peut pas être fait avec des sémaphores.
les variables de sémaphore et de condition sont très similaires et sont principalement utilisées aux mêmes fins. Cependant, il existe des différences mineures qui pourraient en faire un préférable. Par exemple, pour implémenter la synchronisation de barrière, vous ne pourriez pas utiliser de sémaphore. Mais une variable de condition est idéale.
La synchronisation de barrière se produit lorsque vous voulez que tous vos threads attendent que tout le monde soit arrivé à une certaine partie de la fonction de thread. cela peut être implémenté en ayant une variable statique qui est initialement la valeur du nombre total de threads décrémenté par chaque thread lorsqu'il atteint cette barrière. cela voudrait dire que nous voulons que chaque thread dorme jusqu'à l'arrivée du dernier. Un sémaphore ferait exactement le contraire! avec un sémaphore, chaque thread continuera à s'exécuter et le dernier thread (qui définira la valeur du sémaphore sur 0) s'endormira.
une variable d'état, par contre, est idéale. lorsque chaque thread arrive à la barrière, nous vérifions si notre compteur statique est égal à zéro. sinon, nous mettons le thread en veille avec la fonction wait de la variable condition. quand le dernier thread arrive à la barrière, la valeur du compteur sera décrémentée à zéro et ce dernier thread appellera la fonction signal variable de condition qui réveillera tous les autres threads!
Je classe les variables de condition sous la synchronisation du moniteur. J'ai généralement vu les sémaphores et les moniteurs comme deux styles de synchronisation différents. Il existe des différences entre les deux en ce qui concerne la quantité de données d'état conservée de manière inhérente et la manière dont vous souhaitez modéliser le code - mais il n'y a vraiment aucun problème qui puisse être résolu par l'un mais pas par l'autre.
J'ai tendance à coder vers la forme de moniteur; dans la plupart des langues dans lesquelles je travaille, cela revient à des mutex, des variables de condition et certaines variables d'état du support. Mais les sémaphores feraient aussi le travail.