à partir de Rails 4 , tout devrait fonctionner dans un environnement threadé par défaut. Ce que cela signifie, c'est tout le code que nous écrivons ET TOUTES les gemmes que nous utilisons doivent êtrethreadsafe
donc, j'ai quelques questions à ce sujet:
- Qu'est-ce qui n'est PAS thread-safe dans ruby / rails? Vs Qu'est-ce que le thread-safe dans ruby / rails?
- Y at - il une liste des pierres précieuses qui est connu pour être threadsafe ou vice-versa?
- existe-t-il une liste de modèles de code courants qui ne sont PAS un exemple threadsafe
@result ||= some_method
? - Les structures de données dans le noyau ruby lang telles que
Hash
etc threadsafe? - Sur l'IRM, où il y a un
GVL
/GIL
qui signifie qu'un seul thread ruby peut s'exécuter à la fois sauf pourIO
, le changement de threadsafe nous affecte-t-il?
ruby
multithreading
concurrency
thread-safety
ruby-on-rails-4
CuriousMind
la source
la source
Réponses:
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'écriturereturn 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, eta.foo(b)
est ok, si la méthodefoo
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:
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:
find_stuff
fonctionne bien la première fois qu'il est utilisé, mais renvoie autre chose la deuxième fois. Pourquoi? Ilload_things
se trouve que la méthode pense qu'elle possède le hachage d'options qui lui est passé, et le faitcolor = options.delete(:color)
. Maintenant, laSTANDARD_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.
la source
STANDARD_OPTIONS = {...}.freeze
pour élever sur des mutations peu profondes@n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
[...], la valeur de la variable partagée par la suite n'est pas déterministe." - Savez-vous si cela diffère entre les versions de Ruby? Par exemple, exécuter votre code sur 1.8 donne des valeurs différentes de@n
, mais sur 1.9 et plus tard, il semble donner systématiquement@n
égal à 300.En plus de la réponse de Theo, j'ajouterais quelques problèmes à rechercher dans Rails en particulier, si vous passez à config.threadsafe!
Variables de classe :
@@i_exist_across_threads
ENV :
ENV['DONT_CHANGE_ME']
Fils :
Thread.start
la source
Ce n'est pas correct à 100%. Thread-safe Rails est uniquement activé par défaut. Si vous déployez sur un serveur d'applications multi-processus comme Passenger (communauté) ou Unicorn, il n'y aura aucune différence. Ce changement ne vous concerne que si vous déployez sur un environnement multi-thread comme Puma ou Passenger Enterprise> 4.0
Dans le passé, si vous vouliez déployer sur un serveur d'applications multithreads, vous deviez activer config.threadsafe , qui est maintenant par défaut, car tout ce qu'il faisait n'avait aucun effet ou était également appliqué à une application Rails s'exécutant dans un seul processus ( Prooflink ).
Mais si vous voulez tous les avantages de la diffusion en continu de Rails 4 et d'autres éléments en temps réel du déploiement multi-thread, vous trouverez peut-être cet article intéressant. Comme @Theo triste, pour une application Rails, il vous suffit en fait d'omettre l'état statique de mutation lors d'une requête. Bien que ce soit une pratique simple à suivre, vous ne pouvez malheureusement pas en être sûr pour chaque bijou que vous trouverez. Autant que je me souvienne, Charles Oliver Nutter du projet JRuby avait quelques conseils à ce sujet dans ce podcast.
Et si vous voulez écrire une programmation Ruby concurrente pure, où vous auriez besoin de certaines structures de données accessibles par plus d'un thread, vous trouverez peut-être le gem thread_safe utile.
la source