web-dev-qa-db-fra.com

comment savoir ce qui n'est PAS thread-safe en rubis?

à partir de Rails 4 , tout devrait fonctionner par défaut dans un environnement threadé. Cela signifie que tout le code que nous écrivons [~ # ~] et [~ # ~] [~ # ~] tous [~ # ~] les gemmes que nous utilisons doivent être threadsafe

donc, j'ai quelques questions à ce sujet:

  1. qu'est-ce qui n'est PAS thread-safe dans Ruby/rails? Vs Qu'est-ce que le thread-safe dans Ruby/rails?
  2. Existe-t-il une liste de gemmes qui est connues pour être threadsafe ou vice-versa?
  3. existe-t-il une liste de modèles de code courants qui ne sont PAS des exemples de threadsafe @result ||= some_method?
  4. Les structures de données dans Ruby noyau lang comme Hash etc threadsafe?
  5. Sur l'IRM, où il y a GVL/GIL ce qui signifie seulement 1 Ruby peut fonctionner à la fois sauf pour IO, le changement de threadsafe nous affecte-t-il?
89
CuriousMind

Aucune des structures de données de base n'est thread-safe. La seule que je connaisse qui soit livrée avec Ruby est l'implémentation de la file d'attente dans la bibliothèque standard (require 'thread'; q = Queue.new).

Le GIL de l'IRM ne nous sauve pas des problèmes de sécurité des threads. Il s'assure seulement que deux threads ne peuvent pas s'exécuter Ruby code en même temps, c'est-à-dire sur deux CPU différents en même temps. Les threads peuvent toujours être suspendus 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 en mutant 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 seul noyau système, il ne change pas les questions fondamentales de l’écriture de programmes simultanés 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 produisent dans un ordre non déterministe et un rappel frappe le résultat d'un autre. Les systèmes asynchrones à un seul thread 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 frappent l'édition sur un message Stack Overflow plus ou moins en même temps, passez un peu de temps à éditer le message, puis appuyez sur enregistrer, dont les modifications seront vues par un troisième utilisateur plus tard quand ils lire ce même post?

Dans Ruby, comme dans la plupart des autres exécutions simultanées, tout ce qui est plus d'une opération n'est pas thread-safe. @n += 1 N'est pas sûr pour les threads, car il s'agit de plusieurs opérations. @n = 1 Est thread-safe car c'est 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 il est "thread safe", mais à la fin vous le ferez pas obtenir des résultats incohérents des affectations). @n ||= 1 Ne l'est pas et aucune autre opération raccourcie + affectation ne l'est non plus. Une erreur que j'ai commise à plusieurs reprises est d'écrire return unless @started; @started = true, Qui n'est pas du tout sûr pour les threads.

Je ne connais aucune liste faisant autorité d'instructions thread-safe et non-thread safe pour Ruby, mais il existe une règle empirique 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 ok aussi, et a.foo(b) est ok, si la méthode foo est un effet secondaire free (puisque presque tout dans Ruby est un appel de méthode, même une affectation dans de nombreux cas, cela vaut également pour les autres exemples). Les effets secondaires dans ce contexte signifient des choses qui changent def foo(x); @x = x; end est pas sans effet secondaire.

L'une des choses les plus difficiles à écrire du 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 modifiable, les choses peuvent vraiment se gâter. 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 des 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'accesseur stuff. En plus d'être problématique du point de vue de l'encapsulation, il 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 faire ce qu'il veut avec.

Un autre classique Ruby exemple est le suivant:

STANDARD_OPTIONS = {:color => 'red', :count => 10}

def find_stuff
  @some_service.load_things('stuff', STANDARD_OPTIONS)
end

find_stuff Fonctionne très bien la première fois qu'il est utilisé, mais renvoie quelque chose d'autre la deuxième fois. Pourquoi? Il se trouve que la méthode load_things Pense qu'elle possède le hachage des options qui lui est passé et qu'elle color = options.delete(:color). Maintenant, la constante STANDARD_OPTIONS 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. Imaginez 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 accessibles par plusieurs threads), la sécurité des threads n'est pas si difficile. Essayez de minimiser les parties de votre application auxquelles vous accédez simultanément et concentrez-vous sur vos efforts. IIRC, dans une application Rails, un nouvel objet contrôleur est créé pour chaque demande, donc il ne sera utilisé que par un seul thread, et il en va de même pour tous les objets de modèle que vous créez à partir de Cependant, Rails encourage également l'utilisation de variables globales (User.find(...) utilise la variable globale User, vous pouvez la 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 parce que c'est pratique. Soyez très prudent lorsque vous utilisez quoi que ce soit 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 Rails j'irais même jusqu'à dire que vous ne vous inquiétez pas de la sécurité des threads en ce qui concerne Rails lui-même. Vous pouvez toujours créer Rails applications qui ne sont pas thread-safe en faisant les choses que je mentionne ci-dessus. Quand il s'agit d'autres gemmes supposent qu'ils ne sont pas sûrs pour les threads à moins qu'ils disent qu'ils le sont, et s'ils disent qu'ils le supposent pas, et regardez à travers leur code (mais juste parce que vous voyez cela ils vont des choses comme @n ||= 1 ne signifie pas qu'ils ne sont pas sûrs pour les threads, c'est une chose parfaitement légitime à faire dans le bon contexte - vous devriez plutôt chercher des choses comme l'état mutable dans les variables globales, comment il gère les mutables objets passés à ses méthodes, et notamment comment il gère les hachages d'options).

Enfin, être thread-safe n'est pas une propriété transitive. Tout ce qui utilise quelque chose qui n'est pas sûr pour les threads n'est pas lui-même sûr pour les threads.

104
Theo

En plus de la réponse de Theo, j'ajouterais quelques problèmes à rechercher dans Rails spécifiquement, si vous passez à config.threadsafe!

  • Variables de classe:

    @@i_exist_across_threads

  • ENV :

    ENV['DONT_CHANGE_ME']

  • Fils:

    Thread.start

10
crizCraig

à partir de Rails 4, tout devrait fonctionner dans un environnement thread par défaut

Ce n'est pas correct à 100%. Thread-safe Rails est juste activé par défaut. Si vous déployez sur un serveur d'application multi-processus comme Passenger (communauté) ou Unicorn, il n'y aura aucune différence. Cette modification 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 multi-thread, vous deviez activer config.threadsafe , qui est par défaut maintenant, car tout cela n'a eu aucun effet ou s'est également appliqué à une application Rails exécutée en un seul processus ( Prooflink ).

Mais si vous voulez tous les avantages Rails 4 streaming et autres trucs en temps réel du déploiement multi-thread alors vous trouverez peut-être this = article intéressant. Comme @Theo triste, pour une application Rails, il vous suffit en fait d'omettre la mutation de l'état statique lors d'une demande. Bien que ce soit une pratique simple à suivre, malheureusement, vous ne pouvez pas être sûr de ceci pour chaque bijou que vous trouvez. Autant que je me souvienne, Charles Oliver Nutter du projet JRuby avait quelques conseils à ce sujet dans le podcast this .

Et si vous voulez écrire une pure programmation Ruby programmation, où vous auriez besoin de certaines structures de données accessibles par plus d'un thread, vous trouverez peut-être la gemme thread_safe utile.

8
dre-hh