Les implémentations actuelles «sans verrouillage» suivent le même schéma la plupart du temps:
- * lire un état et en faire une copie **
- * modifier la copie **
- faire une opération verrouillée
- réessayer en cas d'échec
(* facultatif: dépend de la structure de données / de l'algorithme)
Le dernier bit est étrangement similaire à un verrou tournant. En fait, c'est un spinlock de base . :)
Je suis d'accord avec @nobugz sur ce point: le coût des opérations imbriquées utilisées dans le multi-threading sans verrouillage est dominé par les tâches de cache et de cohérence mémoire qu'il doit effectuer .
Ce que vous gagnez cependant avec une structure de données "sans verrouillage", c'est que vos "verrous" sont très fins . Cela diminue la chance que deux threads simultanés accèdent au même «verrou» (emplacement mémoire).
L'astuce la plupart du temps est que vous n'avez pas de verrous dédiés - à la place, vous traitez par exemple tous les éléments d'un tableau ou tous les nœuds d'une liste liée comme un "verrou tournant". Vous lisez, modifiez et essayez de mettre à jour s'il n'y a pas eu de mise à jour depuis votre dernière lecture. Si tel était le cas, vous réessayez.
Cela rend votre "verrouillage" (oh, désolé, non verrouillable :) très fin, sans introduire de mémoire ou de ressources supplémentaires.
Le rendre plus fin diminue la probabilité d'attentes. Le rendre aussi fin que possible sans introduire de besoins en ressources supplémentaires semble bien, n'est-ce pas?
Cependant, la plupart du plaisir peut provenir de la bonne commande de chargement / magasin .
Contrairement à nos intuitions, les processeurs sont libres de réorganiser les lectures / écritures de la mémoire - ils sont d'ailleurs très intelligents: vous aurez du mal à observer cela à partir d'un seul thread. Cependant, vous rencontrerez des problèmes lorsque vous commencerez à faire du multi-threading sur plusieurs cœurs. Vos intuitions vont s'effondrer: ce n'est pas parce qu'une instruction est plus tôt dans votre code que cela se produira plus tôt. Les processeurs peuvent traiter les instructions dans le désordre: et ils aiment particulièrement faire cela aux instructions avec accès mémoire, pour masquer la latence de la mémoire principale et mieux utiliser leur cache.
Maintenant, il est sûr contre l'intuition qu'une séquence de code ne circule pas "de haut en bas", mais fonctionne comme s'il n'y avait aucune séquence du tout - et peut être appelée "terrain de jeu du diable". Je pense qu'il est impossible de donner une réponse exacte quant aux réorganisations de chargement / magasin qui auront lieu. Au lieu de cela, on parle toujours en termes de mays and mights and cannettes et se prépare au pire. "Oh, le CPU pourrait réorganiser cette lecture pour venir avant cette écriture, il est donc préférable de mettre une barrière de mémoire ici, à cet endroit."
Les choses sont compliquées par le fait que même ces mays et mights peuvent différer selon les architectures de CPU. Il se peut , par exemple, que quelque chose qui ne se produise pas dans une architecture puisse se produire sur une autre.
Pour obtenir un multi-threading "sans verrouillage", vous devez comprendre les modèles de mémoire.
Obtenir le modèle de mémoire et les garanties corrects n'est cependant pas anodin, comme le démontre cette histoire, dans laquelle Intel et AMD ont apporté quelques corrections à la documentation pour MFENCE
provoquer des remous parmi les développeurs JVM . Il s'est avéré que la documentation sur laquelle les développeurs se sont appuyés depuis le début n'était pas si précise en premier lieu.
Les verrous dans .NET entraînent une barrière de mémoire implicite, vous pouvez donc les utiliser en toute sécurité (la plupart du temps, c'est-à-dire ... voir par exemple cette grandeur de Joe Duffy - Brad Abrams - Vance Morrison sur l'initialisation paresseuse, les verrous, les volatiles et la mémoire barrières. :) (Assurez-vous de suivre les liens sur cette page.)
En prime, vous découvrirez le modèle de mémoire .NET lors d'une quête parallèle . :)
Il y a aussi un "oldie but goldie" de Vance Morrison: ce que chaque développeur doit savoir sur les applications multithread .
... et bien sûr, comme @Eric l'a mentionné, Joe Duffy est une lecture définitive sur le sujet.
Un bon STM peut se rapprocher le plus possible du verrouillage fin et offrira probablement une performance proche ou comparable à une implémentation faite à la main. L'un d'eux est STM.NET des projets DevLabs de MS.
Si vous n'êtes pas un fanatique uniquement de .NET, Doug Lea a fait un excellent travail dans JSR-166 .
Cliff Click a une vision intéressante des tables de hachage qui ne reposent pas sur le verrouillage par bandes - comme le font les tables de hachage simultanées Java et .NET - et semblent bien évoluer jusqu'à 750 processeurs.
Si vous n'avez pas peur de vous aventurer dans le territoire Linux, l'article suivant fournit plus d'informations sur les éléments internes des architectures de mémoire actuelles et sur la façon dont le partage de la ligne de cache peut détruire les performances: ce que tout programmeur doit savoir sur la mémoire .
@Ben a fait de nombreux commentaires sur MPI: Je suis sincèrement d'accord que MPI peut briller dans certains domaines. Une solution basée sur MPI peut être plus facile à raisonner, plus facile à implémenter et moins sujette aux erreurs qu'une implémentation de verrouillage à moitié cuite qui tente d'être intelligente. (C'est cependant - subjectivement - également vrai pour une solution basée sur STM.) Je parierais aussi qu'il est à des années-lumière plus facile d'écrire correctement une application distribuée décente dans par exemple Erlang, comme le suggèrent de nombreux exemples réussis.
MPI, cependant, a ses propres coûts et ses propres problèmes lorsqu'il est exécuté sur un seul système multicœur . Par exemple, à Erlang, il y a des problèmes à résoudre autour de la synchronisation de la planification des processus et des files d'attente de messages .
En outre, à la base, les systèmes MPI implémentent généralement une sorte d' ordonnancement N: M coopératif pour les «processus légers». Cela signifie par exemple qu'il y a un changement de contexte inévitable entre les processus légers. Il est vrai que ce n'est pas un "changement de contexte classique" mais surtout une opération de l'espace utilisateur et cela peut être fait rapidement - cependant je doute sincèrement qu'il puisse être ramené sous les 20 à 200 cycles qu'une opération verrouillée prend . La commutation de contexte en mode utilisateur est certainement plus lentmême dans la bibliothèque Intel McRT. L'ordonnancement N: M avec des processus légers n'est pas nouveau. Les LWP étaient là depuis longtemps dans Solaris. Ils ont été abandonnés. Il y avait des fibres dans NT. Ils sont pour la plupart une relique maintenant. Il y avait des "activations" dans NetBSD. Ils ont été abandonnés. Linux avait sa propre vision du sujet du thread N: M. Il semble être un peu mort maintenant.
De temps en temps, il y a de nouveaux prétendants: par exemple McRT d'Intel , ou plus récemment la planification en mode utilisateur avec ConCRT de Microsoft.
Au niveau le plus bas, ils font ce que fait un planificateur MPI N: M. Erlang - ou n'importe quel système MPI - pourrait grandement bénéficier des systèmes SMP en exploitant le nouvel UMS .
Je suppose que la question du PO ne porte pas sur les mérites et les arguments subjectifs pour / contre toute solution, mais si je devais y répondre, je suppose que cela dépend de la tâche: pour créer des structures de données de base de bas niveau et de haute performance qui fonctionnent sur un système unique avec nombreux cœurs , des techniques à faible verrouillage / «sans verrouillage» ou un STM donneront les meilleurs résultats en termes de performances et battraient probablement une solution MPI à tout moment en termes de performances, même si les plis ci-dessus sont aplatis par exemple à Erlang.
Pour construire quelque chose de modérément plus complexe qui fonctionne sur un seul système, je choisirais peut-être un verrouillage classique à gros grains ou, si les performances sont très préoccupantes, un STM.
Pour construire un système distribué, un système MPI ferait probablement un choix naturel.
Notez qu'il existe également des implémentations MPI pour .NET (bien qu'elles ne semblent pas aussi actives).