Spinlock et sémaphore diffèrent principalement par quatre choses :
Un spinlock est une implémentation possible d'un verrou, à savoir celle qui est implémentée par attente occupée ("spinning"). Un sémaphore est une généralisation d'un verrou (ou, à l'inverse, un verrou est un cas particulier de sémaphore). Habituellement, mais pas nécessairement , les verrous tournants ne sont valides que dans un processus alors que les sémaphores peuvent également être utilisés pour synchroniser entre différents processus.
Un verrou fonctionne pour l'exclusion mutuelle, c'est-à-dire un thread à la fois peut acquérir le verrou et poursuivre avec une "section critique" de code. Habituellement, cela signifie du code qui modifie certaines données partagées par plusieurs threads.
Un sémaphore possède un compteur et se laissera acquérir par un ou plusieurs threads, en fonction de la valeur que vous y publiez et (dans certaines implémentations) en fonction de sa valeur maximale autorisée.
Dans cette mesure, on peut considérer un verrou comme un cas particulier de sémaphore avec une valeur maximale de 1.
Comme indiqué ci-dessus, un spinlock est un verrou, et donc un mécanisme d'exclusion mutuelle (strictement 1 contre 1). Il fonctionne en interrogeant et/ou en modifiant à plusieurs reprises 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 CPU pendant une longue période (peut-être pour toujours !) alors qu'elle n'obtient effectivement "rien".
La principale incitation à une telle approche est le fait qu'un changement de contexte a une surcharge équivalente à quelques centaines (ou peut-être mille) de rotations, donc si un verrou peut être acquis en brûlant quelques cycles de rotation, cela peut très bien être globalement plus efficace. De plus, pour les applications en temps réel, il peut ne pas être acceptable de bloquer et d'attendre que le planificateur revienne vers elles à un moment lointain dans le futur.
Un sémaphore, en revanche, soit ne tourne pas du tout, soit ne tourne que très peu de temps (comme optimisation pour éviter la surcharge des appels système). Si un sémaphore ne peut pas être acquis, il se bloque, cédant du temps CPU à un autre thread prêt à s'exécuter. 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 (ce n'est généralement pas le cas), cela peut être une approche très efficace et économe en CPU.
C'est une idée fausse courante que les spinlocks ou les algorithmes sans verrouillage sont "généralement plus rapides", ou qu'ils ne sont utiles que pour des "tâches très courtes" (idéalement, aucun objet de synchronisation ne devrait être conservé plus longtemps que nécessaire, jamais).
La seule différence importante est la façon dont les différentes approches se comportent en présence de congestion .
Un système bien conçu a normalement peu ou pas de congestion (cela signifie que tous les threads n'essaient pas d'acquérir le verrou exactement au même moment). Par exemple, on ne ferait normalement pas écrire du code qui acquiert un verrou, puis charge un demi-mégaoctet de données compressées en zip à partir du réseau, décode et analyse les données, et enfin modifie une référence partagée (ajoute 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 .
Étant donné que 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 se trouve à 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 essaieront d'acquérir le verrou en même temps (si cela n'a pas pu arriver que vous n'auriez pas besoin d'une serrure !), mais c'est plutôt l'exception que la règle dans un système "sain".
Dans un tel cas, un spinlock beaucoup surpasse un sémaphore car s'il n'y a pas d'encombrement de verrou, la surcharge d'acquisition du verrou d'attente n'est que d'une 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 forte congestion, ou si le verrou est maintenu pendant de longues périodes (parfois vous ne pouvez pas vous en empêcher !), un verrou tournant brûlera des quantités insensées de cycles CPU pour ne rien obtenir.
Un sémaphore (ou mutex) est un bien meilleur choix dans ce cas, car il permet à un thread différent de s'exécuter utile tâches pendant cette période. Ou, si aucun autre thread n'a quelque chose d'utile à faire, cela permet au système d'exploitation de ralentir le processeur et de réduire la chaleur/conserver l'énergie.
De plus, sur un système monocœur, un spinlock sera assez inefficace en présence d'encombrement de verrou, car un thread en rotation perdra tout son temps à attendre un changement d'état qui ne peut pas se produire (pas avant que le thread de libération ne soit programmé, ce qui ça ne se passe pas pendant que le fil d'attente est en cours d'exécution !). Par conséquent, étant donné tout nombre de conflits, l'acquisition du verrou prend environ 1 1/2 tranches de temps dans le meilleur des cas (en supposant que le thread de libération est le prochain planifié), ce qui n'est pas un très bon comportement.
De nos jours, un sémaphore enveloppe généralement sys_futex
sous Linux (éventuellement avec un spinlock qui se ferme après quelques tentatives).
Un verrou tournant est généralement implémenté à l'aide d'opérations atomiques et sans utiliser quoi que ce soit fourni par le système d'exploitation. Dans le passé, cela signifiait utiliser soit des éléments intrinsèques du compilateur, soit des instructions d'assembleur non portables. Pendant ce temps, C++11 et C11 ont des opérations atomiques dans le cadre du langage, donc en dehors de la difficulté générale d'écrire du code sans verrou correct, il est maintenant possible d'implémenter du code sans verrou dans un environnement entièrement portable et (presque) manière indolore.
Au-delà de ce que Yoav Aviram et gbjbaanb ont dit, l'autre point clé était que vous n'utiliseriez jamais un spin-lock sur une machine à un seul processeur, alors qu'un sémaphore aurait du sens sur une telle machine. De nos jours, vous avez souvent du mal à trouver une machine sans plusieurs cœurs, ou hyperthreading, ou équivalent, mais dans les circonstances où vous n'avez qu'un seul processeur, vous devez utiliser des sémaphores. (J'espère que la raison est évidente. Si le processeur unique est occupé à attendre que quelque chose d'autre libère le verrou tournant, mais qu'il s'exécute sur le seul processeur, il est peu probable que le verrou soit libéré tant que le processus ou le thread en cours n'est pas préempté par le système d'exploitation, ce qui peut prendre un certain temps et rien d'utile ne se produit jusqu'à ce que la préemption se produise.)
très simplement, un sémaphore est un objet de synchronisation "rendant", un spinlock est un "busywait". (il y a un peu plus aux sémaphores dans la mesure où ils synchronisent plusieurs threads, contrairement à un mutex ou un garde ou un moniteur ou une section critique qui protège une région de code d'un seul thread)
Vous utiliseriez un sémaphore dans plus de circonstances, mais utilisez un verrou tournant où vous allez verrouiller pendant très peu de temps - le verrouillage a un coût, surtout si vous verrouillez beaucoup. Dans de tels cas, il peut être plus efficace de verrouiller pendant un certain temps en attendant que la ressource protégée soit déverrouillée. Évidemment, il y a un impact sur les performances si vous tournez trop longtemps.
généralement, si vous tournez plus longtemps qu'un quantum de thread, vous devez utiliser un sémaphore.