Aucune des structures de données de base n'est thread-safe. Le seul que je connaisse qui soit livré avec Ruby est l'implémentation de la file d'attente dans la bibliothèque standard ( require 'thread'; q = Queue.new
).
Le GIL de MRI ne nous sauve pas des problèmes de sécurité des fils. Il s'assure seulement que deux threads ne peuvent pas exécuter du code Ruby en même temps , c'est-à-dire sur deux processeurs différents en même temps. Les threads peuvent toujours être interrompus et repris à tout moment dans votre code. Si vous écrivez du code comme @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
par exemple la mutation d'une variable partagée à partir de plusieurs threads, la valeur de la variable partagée par la suite n'est pas déterministe. Le GIL est plus ou moins une simulation d'un système central unique, il ne change pas les problèmes fondamentaux de l'écriture de programmes concurrents corrects.
Même si l'IRM avait été monothread comme Node.js, vous devriez toujours penser à la concurrence. L'exemple avec la variable incrémentée fonctionnerait bien, mais vous pouvez toujours obtenir des conditions de concurrence où les choses se passent dans un ordre non déterministe et un rappel écrase le résultat d'un autre. Les systèmes asynchrones à thread unique sont plus faciles à raisonner, mais ils ne sont pas exempts de problèmes de concurrence. Pensez simplement à une application avec plusieurs utilisateurs: si deux utilisateurs appuient sur modifier un article Stack Overflow plus ou moins en même temps, passez un peu de temps à éditer l'article, puis appuyez sur Enregistrer, dont les modifications seront vues par un troisième utilisateur plus tard lorsqu'ils lire ce même message?
Dans Ruby, comme dans la plupart des autres environnements d'exécution simultanés, tout ce qui est plus d'une opération n'est pas thread-safe. @n += 1
n'est pas thread-safe, car il s'agit d'opérations multiples. @n = 1
est thread-safe car il s'agit d'une opération (c'est beaucoup d'opérations sous le capot, et j'aurais probablement des ennuis si j'essayais de décrire en détail pourquoi c'est "thread safe", mais à la fin, vous n'obtiendrez pas de résultats incohérents avec les affectations ). @n ||= 1
, n'est pas et aucune autre opération abrégée + affectation ne l'est non plus. Une erreur que j'ai commise à plusieurs reprises est l'écriture return unless @started; @started = true
, qui n'est pas du tout thread-safe.
Je ne connais pas de liste faisant autorité d'instructions thread-safe et non thread-safe pour Ruby, mais il existe une règle simple: si une expression ne fait qu'une seule opération (sans effet secondaire), elle est probablement thread-safe. Par exemple: a + b
est ok, a = b
est également ok, et a.foo(b)
est ok, si la méthode foo
est sans effet secondaire (puisque à peu près tout dans Ruby est un appel de méthode, même une affectation dans de nombreux cas, cela vaut aussi pour les autres exemples). Les effets secondaires dans ce contexte signifient des choses qui changent d'état. def foo(x); @x = x; end
n'est pas sans effets secondaires.
L'un des aspects les plus difficiles de l'écriture de code thread-safe dans Ruby est que toutes les structures de données de base, y compris le tableau, le hachage et la chaîne, sont mutables. Il est très facile de divulguer accidentellement une partie de votre état, et lorsque cette pièce est mutable, les choses peuvent devenir vraiment foutues. Considérez le code suivant:
class Thing
attr_reader :stuff
def initialize(initial_stuff)
@stuff = initial_stuff
@state_lock = Mutex.new
end
def add(item)
@state_lock.synchronize do
@stuff << item
end
end
end
Une instance de cette classe peut être partagée entre les threads et ils peuvent y ajouter des éléments en toute sécurité, mais il y a un bogue de concurrence (ce n'est pas le seul): l'état interne de l'objet fuit via l' stuff
accesseur. En plus d'être problématique du point de vue de l'encapsulation, cela ouvre également une boîte de vers de concurrence. Peut-être que quelqu'un prend ce tableau et le transmet ailleurs, et ce code pense à son tour qu'il possède maintenant ce tableau et peut en faire ce qu'il veut.
Un autre exemple classique de Ruby est le suivant:
STANDARD_OPTIONS = {:color => 'red', :count => 10}
def find_stuff
@some_service.load_things('stuff', STANDARD_OPTIONS)
end
find_stuff
fonctionne bien la première fois qu'il est utilisé, mais renvoie autre chose la deuxième fois. Pourquoi? Il load_things
se trouve que la méthode pense qu'elle possède le hachage d'options qui lui est passé, et le fait color = options.delete(:color)
. Maintenant, la STANDARD_OPTIONS
constante n'a plus la même valeur. Les constantes ne sont constantes que dans ce qu'elles référencent, elles ne garantissent pas la constance des structures de données auxquelles elles se réfèrent. Pensez simplement à ce qui se passerait si ce code était exécuté simultanément.
Si vous évitez l'état mutable partagé (par exemple, les variables d'instance dans les objets accessibles par plusieurs threads, les structures de données comme les hachages et les tableaux auxquels accèdent plusieurs threads), la sécurité des threads n'est pas si difficile. Essayez de réduire au minimum les parties de votre application auxquelles vous accédez simultanément et concentrez vos efforts là-bas. IIRC, dans une application Rails, un nouvel objet contrôleur est créé pour chaque requête, il ne sera donc utilisé que par un seul thread, et il en va de même pour tous les objets modèle que vous créez à partir de ce contrôleur. Cependant, Rails encourage également l'utilisation de variables globales ( User.find(...)
utilise la variable globaleUser
, vous pouvez le considérer uniquement comme une classe, et c'est une classe, mais c'est aussi un espace de noms pour les variables globales), certaines d'entre elles sont sûres car elles sont en lecture seule, mais parfois vous enregistrez des choses dans ces variables globales car elles est pratique. Soyez très prudent lorsque vous utilisez tout ce qui est globalement accessible.
Il est possible d'exécuter Rails dans des environnements filetés depuis un certain temps maintenant, donc sans être un expert de Rails, j'irais encore jusqu'à dire que vous n'avez pas à vous soucier de la sécurité des threads quand il s'agit de Rails lui-même. Vous pouvez toujours créer des applications Rails qui ne sont pas thread-safe en faisant certaines des choses que j'ai mentionnées ci-dessus. Quand il s'agit, d'autres gemmes supposent qu'elles ne sont pas sûres pour les threads à moins qu'elles ne disent qu'elles le sont, et si elles disent qu'elles supposent qu'elles ne le sont pas, et regardent leur code (mais simplement parce que vous voyez qu'elles font des choses comme@n ||= 1
ne signifie pas qu'ils ne sont pas thread-safe, c'est une chose parfaitement légitime à faire dans le bon contexte - vous devriez plutôt rechercher des choses comme l'état mutable dans les variables globales, comment il gère les objets mutables passés à ses méthodes, et surtout comment il gère les hachages d'options).
Enfin, être thread unsafe est une propriété transitive. Tout ce qui utilise quelque chose qui n'est pas thread-safe n'est pas thread-safe en soi.