Articles

Spinlock vs Sémaphore

Un spinlock est un verrou, et donc un mécanisme d’exclusion mutuelle (strictement 1 à 1). Il fonctionne en interrogeant et/ou modifiant de manière répétée un emplacement mémoire, généralement de manière atomique. Cela signifie que l’acquisition d’un spinlock est une opération « occupée » qui brûle éventuellement des cycles de CPU pendant une longue période (peut-être pour toujours !) alors qu’elle n’accomplit effectivement « rien ».
La principale motivation pour une telle approche est le fait qu’un changement de contexte a un surcoût équivalent à tourner quelques centaines (ou peut-être des milliers) de fois, donc si un verrou peut être acquis en brûlant quelques cycles en tournant, cela peut globalement très bien être plus efficace. En outre, pour les applications en temps réel, il peut ne pas être acceptable de bloquer et d’attendre que le planificateur revienne à eux à un moment lointain dans le futur.

#include <boost/atomic.hpp>
class spinlock {
private:
typedef enum {Locked, Unlocked} LockState;
boost::atomic<LockState> state_;
public:
spinlock() : state_(Unlocked) {}
void lock()
{
while (state_.exchange(Locked, boost::memory_order_acquire) == Locked) {
/* busy-wait */
}
}
void unlock()
{
state_.store(Unlocked, boost::memory_order_release);
}
};// Usagespinlock s;
s.lock();
// access data structure here
s.unlock();

Un sémaphore, par contraste, soit ne tourne pas du tout, soit ne tourne que pendant un temps très court (comme une optimisation pour éviter l’overhead syscall). Si un sémaphore ne peut être acquis, il se bloque, cédant du temps CPU à un autre thread qui est prêt à fonctionner. Cela peut bien sûr signifier que quelques millisecondes s’écoulent avant que votre thread ne soit à nouveau programmé, mais si ce n’est pas un problème (en général, ce n’est pas le cas), alors cela peut être une approche très efficace et conservatrice du CPU.

Un système bien conçu a normalement une congestion faible ou nulle (cela signifie que tous les threads n’essaient pas d’acquérir le verrou exactement au même moment). Par exemple, on n’écrirait normalement pas un code qui acquiert un verrou, puis charge un demi-mégaoctet de données compressées par zip depuis le réseau, décode et analyse les données, et enfin modifie une référence partagée (ajouter des données à un conteneur, etc.) avant de libérer le verrou. Au lieu de cela, on acquerrait le verrou uniquement dans le but d’accéder à la ressource partagée.
Comme cela signifie qu’il y a considérablement plus de travail à l’extérieur de la section critique qu’à l’intérieur, naturellement la probabilité qu’un thread soit à l’intérieur de la section critique est relativement faible, et donc peu de threads se disputent le verrou en même temps. Bien sûr, de temps en temps, deux threads vont essayer d’acquérir le verrou en même temps (si cela ne pouvait pas arriver, vous n’auriez pas besoin d’un verrou !), mais c’est plutôt l’exception que la règle dans un système « sain ».

Dans un tel cas, un spinlock surpasse largement un sémaphore car s’il n’y a pas de congestion de verrou, l’overhead d’acquisition du spinlock est une simple douzaine de cycles par rapport à des centaines/milliers de cycles pour un changement de contexte ou 10-20 millions de cycles pour perdre le reste d’une tranche de temps.

D’un autre côté, étant donné une congestion élevée, ou si le verrou est maintenu pendant de longues périodes (parfois, vous ne pouvez pas vous en empêcher !), un spinlock brûlera des quantités folles de cycles CPU pour ne rien accomplir.
Un sémaphore (ou mutex) est un bien meilleur choix dans ce cas, car il permet à un autre thread d’exécuter des tâches utiles pendant ce temps. Ou, si aucun autre thread n’a quelque chose d’utile à faire, il permet au système d’exploitation de réduire la puissance du CPU et de réduire la chaleur / conserver l’énergie.