J'utilise un verrou tournant pour protéger une très petite section critique. La contention survient (très) _ rarement, aussi un verrou de rotation est-il plus approprié qu'un mutex ordinaire.
Mon code actuel est le suivant et suppose x86 et GCC:
volatile int exclusion = 0;
void lock() {
while (__sync_lock_test_and_set(&exclusion, 1)) {
// Do nothing. This GCC builtin instruction
// ensures memory barrier.
}
}
void unlock() {
__sync_synchronize(); // Memory barrier.
exclusion = 0;
}
Alors je me demande:
__sync_lock_release
. Je ne suis pas un expert en barrières de mémoire, je ne suis donc pas sûr de pouvoir utiliser ceci au lieu de __sync_synchronize
.Je me fiche de pas du tout de la contention. Il peut y avoir 1, voire 2 autres threads essayant de verrouiller le verrou de rotation une fois tous les quelques jours.
Alors je me demande:
* Is it correct?
Dans le contexte mentionné, je dirais oui.
* Is it optimal?
C'est une question chargée. En réinventant la roue, vous réinventez également de nombreux problèmes résolus par d'autres implémentations.
Je m'attendrais à une boucle de déchets en cas d'échec où vous n'essayez pas d'accéder au mot de verrouillage.
L'utilisation d'une barrière complète dans le déverrouillage nécessite uniquement la sémantique de la publication (c'est pourquoi vous utiliseriez __sync_lock_release, de manière à obtenir st1.rel sur itanium au lieu de mf, ou un lwsync sur powerpc, ...). Si vous ne vous souciez que de x86 ou de x86_64, les types d'obstacles utilisés ici ou non importent peu (mais si vous deviez faire le saut dans l'intel d'Intel pour un port HP-IPF, vous ne voudriez pas cela).
vous n'avez pas l'instruction pause () que vous mettriez normalement avant votre boucle de déchets.
quand il y a conflit vous voulez quelque chose, semop, ou même un sommeil muet en désespoir de cause. Si vous avez vraiment besoin de la performance que cela vous apporte, alors la proposition futex est probablement bonne. Si vous avez besoin de la performance, cela vous rapporte assez mal pour maintenir ce code que vous avez encore beaucoup de recherche à faire.
Notez qu'il y avait un commentaire disant que la barrière de libération n'était pas nécessaire. Ce n'est pas vrai même sur x86 car la barrière de publication sert également d'instruction au compilateur pour ne pas mélanger les autres accès mémoire autour de la "barrière". Cela ressemble beaucoup à ce que vous obtiendriez si vous utilisiez asm ("" ::: "memory").
* on compare and swap
Sur x86, sync_lock_test_and_set mappera sur une instruction xchg qui a un préfixe de verrou implicite. C'est certainement le code généré le plus compact (surtout si vous utilisez un octet pour le "mot de verrouillage" au lieu d'un entier), mais pas moins correct que si vous utilisiez LOCK CMPXCHG. L'utilisation de compare et swap peut être utilisée pour des algorithmes plus sophistiqués (comme placer un pointeur non nul sur les métadonnées du premier "serveur" dans le mot clé en cas d'échec).
Ca me va. Btw, voici le manuel implémentation qui est plus efficace même dans le cas litigieux.
void lock(volatile int *exclusion)
{
while (__sync_lock_test_and_set(exclusion, 1))
while (*exclusion)
;
}
En réponse à vos questions:
__sync_lock_release()
dans le cas unlock()
; car cela réduira le verrou et ajoutera une barrière de mémoire en une seule opération. Cependant, en supposant que votre affirmation selon laquelle il y aura rarement des conflits; ça me semble bien.Si vous utilisez une version récente de Linux, vous pourrez peut-être utiliser un futex - un "mutex d’espace utilisateur rapide":
Un verrou à base de futex correctement programmé n'utilisera pas d'appels système, sauf en cas de contestation du verrou.
Dans le cas non contesté, que vous essayez d'optimiser avec votre spinlock, le futex se comportera comme un spinlock, sans nécessiter d'appel systémique dans le noyau. Si le verrou est contesté, l'attente a lieu dans le noyau sans attente en attente.
Je me demande si la mise en œuvre CAS suivante est la bonne sur x86_64. Il est presque deux fois plus rapide sur mon ordinateur portable i7 X920 (Fedora 13 x86_64, gcc 4.4.5).
inline void lock(volatile int *locked) {
while (__sync_val_compare_and_swap(locked, 0, 1));
asm volatile("lfence" ::: "memory");
}
inline void unlock(volatile int *locked) {
*locked=0;
asm volatile("sfence" ::: "memory");
}
Je ne peux pas commenter sur l'exactitude, mais le titre de votre question a déclenché un drapeau rouge avant même de lire le corps de la question. Les primitives de synchronisation sont diablement difficiles à assurer. Le mieux est d’utiliser une bibliothèque bien conçue/maintenue, peut-être pthreads ou boost :: thread .
Il y a quelques fausses hypothèses.
Premièrement, SpinLock n’a de sens que si la ressource est verrouillée sur un autre processeur. Si la ressource est verrouillée sur le même processeur (ce qui est toujours le cas sur les systèmes à un seul processeur), vous devez assouplir le planificateur pour déverrouiller la ressource. Votre code actuel fonctionnera sur un système à un seul processeur, car le planificateur changera automatiquement de tâche, mais ce sera un gaspillage de ressources.
Sur un système multiprocesseur, la même chose peut se produire, mais la tâche peut migrer d'un processeur à un autre. En bref, l'utilisation de spin lock est correcte si vous garantissez que vos tâches seront exécutées sur un processeur différent.
Deuxièmement, verrouiller un mutex IS rapidement (aussi vite que le verrou tournant) quand est déverrouillé. Le verrouillage (et le déverrouillage) des mutex est lent (très lent) uniquement si le mutex est déjà verrouillé.
Donc, dans votre cas, je suggère d'utiliser des mutex.
L’une des améliorations suggérées consiste à utiliser TATAS (test-and-test-and-set). L'utilisation des opérations CAS est considérée comme assez coûteuse pour le processeur, il est donc préférable de les éviter si possible . Autre chose, assurez-vous de ne pas subir d'inversion de priorité (que se passera-t-il si un thread très prioritaire tente d'acquérir le verrou Dans Windows, par exemple, ce problème sera résolu par le planificateur en utilisant une augmentation de priorité, mais vous pouvez abandonner explicitement la tranche de temps de votre fil au cas où vous n’auriez pas réussi à obtenir le verrouiller vos 20 derniers essais (par exemple ..)
Votre procédure de déverrouillage n’a pas besoin de la barrière de mémoire; l'affectation à l'exclusion est atomique tant qu'elle dword est alignée sur le x86.
Dans le cas spécifique de x86 (32/64), je ne pense pas que vous ayez besoin d'une barrière de mémoire dans le code de déverrouillage. x86 n'effectue aucune réorganisation, sauf que les magasins sont d'abord placés dans un tampon de stockage et qu'ils deviennent ainsi visibles peuvent être différés pour d'autres threads. Et un thread qui effectue un stockage puis lit à partir de la même variable lira à partir de son tampon de stockage s'il n'a pas encore été vidé en mémoire. Il suffit donc d’une instruction asm
pour empêcher les réorganisations du compilateur. Vous courez le risque qu'un fil maintienne le verrou légèrement plus long que nécessaire du point de vue des autres fils, mais si vous ne vous souciez pas de la contention, cela ne devrait pas avoir d'importance. En fait, pthread_spin_unlock
est implémenté comme ça sur mon système (linux x86_64).
Mon système implémente également pthread_spin_lock
en utilisant lock decl lockvar; jne spinloop;
au lieu de xchg
(qui est ce que __sync_lock_test_and_set
utilise), mais je ne sais pas s'il existe réellement une différence de performances.