Extrait de code:
a = 0 Array.new(50){ Thread.new { 500_000.times { a += 1 } } }.each(&:join) p "a: #{a}"
Résultat: a = 25_000_000
.
À ma connaissance, (MRI) Ruby utilise GIL, donc là est un seul thread ruby peut obtenir le processeur, mais lorsque le changement de thread se produit, certaines données du thread ruby seront stockées pour restaurer le thread plus tard. Donc, en théorie, a + = 1
peut ne pas être thread-safe.
Mais le résultat ci-dessus s'avère que je me trompe. Ruby rend-il un + = 1
atomique? Si vrai, quelles opérations peuvent être considérées comme thread-safe?
3 Réponses :
Dans votre exemple, la cohérence apparente est en grande partie due au verrouillage global de l'interpréteur, mais aussi en partie à la façon dont votre moteur Ruby et vos séquences de code (théoriquement) des threads asynchrones. Vous obtenez des résultats cohérents car chaque boucle de chaque thread incrémente simplement la valeur actuelle de a , qui n'est pas une variable locale de bloc ou locale de thread. Avec les threads sur la machine virtuelle YARV, un seul thread à la fois inspecte ou définit la valeur actuelle de a , mais je ne dirais pas vraiment que c'est une opération atomique. C'est juste un sous-produit du manque de concurrence en temps réel du moteur entre les threads et de l'implémentation sous-jacente de la machine virtuelle Ruby.
Si vous souhaitez préserver la sécurité des threads dans Ruby sans vous fier à des comportements idiosyncratiques qui semblent cohérents, envisagez d'utiliser une bibliothèque thread-safe telle que concurrent-ruby . Sinon, vous pouvez vous fier à des comportements qui ne sont pas garantis dans les moteurs Ruby ou les versions Ruby.
Par exemple, trois exécutions consécutives de votre code dans JRuby (ce qui a des threads simultanés) donnera généralement des résultats différents à chaque exécution. Par exemple:
# => "a: 3353241"
# => "a: 3088145"
# => "a: 2642263"
"Avec des threads verts […]" - Notez qu'aucune implémentation Ruby existante n'utilise des threads verts. (À moins que, disons, vous exécutiez TruffleRuby ou JRuby sur une JVM ou IronRuby sur un CLI VES qui utilise des threads verts, bien que je ne pense pas qu'il en existe actuellement.) Le dernier à utiliser des threads verts était MRI, qui n'a pas été maintenu par ses développeurs depuis 2013, et a été abandonnée même à partir de la dernière version Long-Term-Support de toute distribution Linux en 2017. YARV, MRuby, TruffleRuby, JRuby, IronRuby, MagLev utilisent tous des threads natifs du système d'exploitation. Opal n'a pas du tout de fils.
Merci pour votre réponse. J'ai obtenu des informations utiles du code source de CRuby sur le multithread, ce qui m'aide à comprendre la confusion: github.com/ruby/ruby/blob/master/vm_eval.c#L110 .
Ruby n'a pas de modèle de mémoire bien défini, donc dans un certain sens philosophique, la question est absurde, car sans modèle de mémoire, le terme "thread-safe" n'est même pas défini. Par exemple, la Spécification du langage ISO Ruby ne documente même pas la Thread
class .
La façon dont les gens écrivent du code simultané dans Ruby sans un modèle de mémoire bien défini est essentiellement "deviner et tester". Vous devinez ce que vont faire les implémentations, puis vous testez autant de versions d'autant d'implémentations que possible sur autant de plates-formes et autant de systèmes d'exploitation sur autant d'architectures de CPU et autant de tailles de système différentes que possible.
Comme vous pouvez le voir dans la réponse de Todd , le simple fait de tester une autre implémentation révèle déjà que votre conclusion était fausse. (Conseil de pro: ne jamais faire une généralisation basée sur une taille d’échantillon de 1!)
L'alternative consiste à utiliser une bibliothèque qui a déjà fait ce qui précède, comme le la bibliothèque concurrent-ruby
mentionnée dans la réponse de Todd. Ils font tous les tests que j'ai mentionnés ci-dessus. Ils travaillent également en étroite collaboration avec les responsables des différentes implémentations. Par exemple. Chris Seaton, le développeur principal de TruffleRuby est également l'un des mainteneurs de concurrent-ruby
, et Charlie Nutter, le développeur principal de JRuby, est l'un des contributeurs.
Mais le résultat ci-dessus s'avère que je me trompe.
Les résultats sont trompeurs. Dans Ruby,
a + = 1
est un raccourci pour:Integer.prepend(ThreadTest) a = 0 Array.new(50){ Thread.new { 500_000.times { a += 1 } } }.each(&:join) p "a: #{a}" #=> "a: 11916339"Avec
a + 1
étant un appel de méthode qui se produit avant l'affectation. Puisque les entiers sont des objets dans Ruby, nous pouvons remplacer cette méthode:module ThreadTest def +(other) super end end Integer.prepend(ThreadTest)Le code ci-dessus ne fait rien d'utile, il appelle simplement
super
. Mais simplement ajouter une implémentation Ruby en plus de l'implémentation C intégrée suffit pour interrompre (ou corriger) votre test:a = a + 1
merci beaucoup, excellent exemple. Alors, puis-je dire que les fonctions C sont sécurisées pour les threads et que les méthodes Ruby ne sont pas sûres?
Eh bien, oui et non. Ruby (MRI / YARV) ne change pas de thread lors de l'exécution d'une méthode C, ce qui explique "pourquoi" votre code se comporte de cette façon. Mais dans votre code, vous ne pouvez pas distinguer une méthode C d'une méthode Ruby. S'appuyer sur un détail d'implémentation d'un interprète spécifique serait très risqué. De plus, a + = 1
sont deux opérations: a + 1
et a = ...
- un fil le basculement pourrait encore se produire entre eux (encore une fois: spécifique à l'implémentation). Si vous voulez avoir du code thread-safe, vous devez utiliser un mutex a>.