comment savoir ce qui n'est PAS thread-safe dans ruby?

93

à 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:

  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. Y at - il une liste des pierres précieuses qui est connu pour être threadsafe ou vice-versa?
  3. existe-t-il une liste de modèles de code courants qui ne sont PAS un exemple threadsafe @result ||= some_method?
  4. Les structures de données dans le noyau ruby ​​lang telles que Hashetc threadsafe?
  5. Sur l'IRM, où il y a un GVL/GIL qui signifie qu'un seul thread ruby ​​peut s'exécuter à la fois sauf pour IO, le changement de threadsafe nous affecte-t-il?
CuriousMind
la source
2
Êtes-vous sûr que tout le code et toutes les gemmes DOIVENT être threadsafe? Ce que disent les notes de publication, c'est que Rails lui-même sera threadsafe, pas que tout le reste utilisé avec lui
DOIT l'
Les tests multi-threads seraient le pire risque possible de threadsafe. Lorsque vous devez modifier la valeur d'une variable d'environnement autour de votre scénario de test, vous n'êtes instantanément pas threadsafe. Comment contourneriez-vous cela? Et oui, toutes les gemmes doivent être threadsafe.
Lukas Oberhuber

Réponses:

110

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 += 1n'est pas thread-safe, car il s'agit d'opérations multiples. @n = 1est 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 + best ok, a = best également ok, et a.foo(b)est ok, si la méthode fooest 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; endn'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' stuffaccesseur. 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_stufffonctionne bien la première fois qu'il est utilisé, mais renvoie autre chose la deuxième fois. Pourquoi? Il load_thingsse 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_OPTIONSconstante 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.

Théo
la source
Très bonne réponse. Étant donné qu'une application de rails typique est multi-processus (comme vous l'avez décrit, de nombreux utilisateurs différents accèdent à la même application), je me demande quel est le risque marginal des threads pour le modèle de concurrence ... En d'autres termes, combien plus "dangereux" est-ce pour fonctionner en mode threadé si vous avez déjà affaire à une concurrence via des processus?
gingerlime
2
@Theo Merci beaucoup. Ce truc constant est une grosse bombe. Ce n'est même pas un processus sûr. Si la constante est modifiée dans une demande, les demandes ultérieures verront la constante modifiée même dans un seul thread. Ruby constants are weird
rubish
5
Faire STANDARD_OPTIONS = {...}.freezepour élever sur des mutations peu profondes
glebm
Vraiment bonne réponse
Cheyne
3
"Si vous écrivez du code comme @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.
user200783
10

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

crizCraig
la source
9

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

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.

dre-hh
la source