Woher wissen, was in Ruby NICHT threadsicher ist?

92

Ab Rails 4 müsste standardmäßig alles in einer Thread-Umgebung ausgeführt werden. Dies bedeutet, dass der gesamte Code, den wir schreiben, UND ALLE Edelsteine, die wir verwenden, vorhanden sein müssenthreadsafe

Daher habe ich einige Fragen dazu:

  1. Was ist in Rubin / Schienen NICHT fadensicher? Vs Was ist in Rubin / Schienen fadensicher?
  2. Gibt es eine Liste der Edelsteine , die ist bekannt THREAD oder umgekehrt zu sein?
  3. Gibt es eine Liste gängiger Codemuster, die KEINE threadsicheren Beispiele sind @result ||= some_method?
  4. Sind die Datenstrukturen in Ruby Lang Core wie Hashetc threadsicher?
  5. Bei der MRT, bei der ein GVL/GIL was bedeutet, dass nur 1 IORubinfaden gleichzeitig ausgeführt werden kann , wirkt sich die threadsichere Änderung auf uns aus?
CuriousMind
quelle
2
Sind Sie sicher, dass der gesamte Code und alle Edelsteine ​​threadsicher sein müssen? In den Versionshinweisen heißt es, dass Rails selbst threadsicher sein wird und nicht, dass alles andere, was damit verwendet wird, sein muss
Enthrops
Multithread-Tests wären das schlechteste threadsichere Risiko. Wenn Sie den Wert einer Umgebungsvariablen um Ihren Testfall herum ändern müssen, sind Sie sofort nicht threadsicher. Wie würden Sie das umgehen? Und ja, alle Edelsteine ​​müssen threadsicher sein.
Lukas Oberhuber

Antworten:

109

Keine der Kerndatenstrukturen ist threadsicher. Das einzige, von dem ich weiß, dass es mit Ruby geliefert wird, ist die Warteschlangenimplementierung in der Standardbibliothek ( require 'thread'; q = Queue.new).

Die GIL von MRI rettet uns nicht vor Fragen der Thread-Sicherheit. Es wird nur sichergestellt, dass nicht zwei Threads Ruby-Code gleichzeitig ausführen können , dh auf zwei verschiedenen CPUs genau zur gleichen Zeit. Threads können weiterhin an jedem Punkt in Ihrem Code angehalten und fortgesetzt werden. Wenn Sie Code schreiben, @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }z. B. eine gemeinsam genutzte Variable aus mehreren Threads mutieren, ist der Wert der gemeinsam genutzten Variablen danach nicht deterministisch. Die GIL ist mehr oder weniger eine Simulation eines einzelnen Kernsystems. Sie ändert nichts an den grundlegenden Problemen beim Schreiben korrekter gleichzeitiger Programme.

Selbst wenn die MRT wie Node.js Single-Threaded gewesen wäre, müssten Sie immer noch über Parallelität nachdenken. Das Beispiel mit der inkrementierten Variablen würde gut funktionieren, aber Sie können immer noch Rennbedingungen erhalten, bei denen die Dinge in nicht deterministischer Reihenfolge ablaufen und ein Rückruf das Ergebnis eines anderen blockiert. Asynchrone Systeme mit einem Thread sind leichter zu verstehen, aber nicht frei von Parallelitätsproblemen. Stellen Sie sich eine Anwendung mit mehreren Benutzern vor: Wenn zwei Benutzer mehr oder weniger gleichzeitig auf "Bearbeiten" für einen Stapelüberlauf-Beitrag klicken, müssen Sie einige Zeit damit verbringen, den Beitrag zu bearbeiten, und dann auf "Speichern" klicken, dessen Änderungen später von einem dritten Benutzer angezeigt werden den gleichen Beitrag lesen?

In Ruby ist, wie in den meisten anderen gleichzeitigen Laufzeiten, alles, was mehr als eine Operation ist, nicht threadsicher. @n += 1ist nicht threadsicher, da es sich um mehrere Operationen handelt. @n = 1ist threadsicher, weil es sich um eine Operation handelt (es sind viele Operationen unter der Haube, und ich würde wahrscheinlich in Schwierigkeiten geraten, wenn ich versuchen würde, detailliert zu beschreiben, warum es "threadsicher" ist, aber am Ende erhalten Sie keine inkonsistenten Ergebnisse aus Zuweisungen ). @n ||= 1, ist nicht und keine andere Kurzoperation + Zuweisung ist auch nicht. Ein Fehler, den ich oft gemacht habe, ist das Schreiben return unless @started; @started = true, das überhaupt nicht threadsicher ist.

Ich kenne keine maßgebliche Liste von thread-sicheren und nicht thread-sicheren Anweisungen für Ruby, aber es gibt eine einfache Faustregel: Wenn ein Ausdruck nur eine (nebenwirkungsfreie) Operation ausführt, ist er wahrscheinlich thread-sicher. Zum Beispiel: a + bist in Ordnung, a = bist auch in Ordnung und a.foo(b)ist in Ordnung, wenn die Methode nebenwirkungsfrei fooist (da fast alles in Ruby ein Methodenaufruf ist, in vielen Fällen sogar eine Zuweisung, gilt dies auch für die anderen Beispiele). Nebenwirkungen bedeuten in diesem Zusammenhang Dinge, die den Zustand ändern. def foo(x); @x = x; endist nicht nebenwirkungsfrei.

Eines der schwierigsten Dinge beim Schreiben von thread-sicherem Code in Ruby ist, dass alle Kerndatenstrukturen, einschließlich Array, Hash und String, veränderbar sind. Es ist sehr leicht, versehentlich ein Stück Ihres Zustands zu verlieren, und wenn dieses Stück veränderlich ist, können die Dinge wirklich durcheinander geraten. Betrachten Sie den folgenden Code:

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

Eine Instanz dieser Klasse kann von Threads gemeinsam genutzt werden und sie können sicher Dinge hinzufügen, aber es gibt einen Parallelitätsfehler (es ist nicht der einzige): Der interne Status des Objekts leckt durch den stuffAccessor. Es ist nicht nur aus Sicht der Kapselung problematisch, sondern eröffnet auch eine Dose Parallelitätswürmer. Vielleicht nimmt jemand dieses Array und gibt es an einen anderen Ort weiter, und dieser Code glaubt wiederum, dass er dieses Array jetzt besitzt und damit machen kann, was er will.

Ein weiteres klassisches Ruby-Beispiel ist folgendes:

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

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

find_stufffunktioniert gut, wenn es zum ersten Mal verwendet wird, gibt aber beim zweiten Mal etwas anderes zurück. Warum? Die load_thingsMethode glaubt zufällig, dass sie den an sie übergebenen Options-Hash besitzt, und tut dies auch color = options.delete(:color). Jetzt hat die STANDARD_OPTIONSKonstante nicht mehr den gleichen Wert. Konstanten sind nur in dem, worauf sie verweisen, konstant, sie garantieren nicht die Konstanz der Datenstrukturen, auf die sie sich beziehen. Stellen Sie sich vor, was passieren würde, wenn dieser Code gleichzeitig ausgeführt würde.

Wenn Sie einen gemeinsamen veränderlichen Status vermeiden (z. B. Instanzvariablen in Objekten, auf die mehrere Threads zugreifen, Datenstrukturen wie Hashes und Arrays, auf die mehrere Threads zugreifen), ist die Thread-Sicherheit nicht so schwierig. Versuchen Sie, die Teile Ihrer Anwendung zu minimieren, auf die gleichzeitig zugegriffen wird, und konzentrieren Sie Ihre Bemühungen dort. IIRC: In einer Rails-Anwendung wird für jede Anforderung ein neues Controller-Objekt erstellt, sodass es nur von einem einzelnen Thread verwendet wird. Dies gilt auch für alle Modellobjekte, die Sie von diesem Controller erstellen. Rails empfiehlt jedoch auch die Verwendung globaler Variablen ( User.find(...)verwendet die globale VariableUserSie können sich das nur als eine Klasse vorstellen, und es ist eine Klasse, aber es ist auch ein Namespace für globale Variablen. Einige davon sind sicher, weil sie schreibgeschützt sind, aber manchmal speichern Sie Dinge in diesen globalen Variablen, weil es ist bequem. Seien Sie sehr vorsichtig, wenn Sie alles verwenden, auf das global zugegriffen werden kann.

Es ist schon seit einiger Zeit möglich, Rails in Thread-Umgebungen auszuführen. Ohne Rails-Experte würde ich immer noch sagen, dass Sie sich keine Sorgen um die Thread-Sicherheit machen müssen, wenn es um Rails selbst geht. Sie können weiterhin Rails-Anwendungen erstellen, die nicht threadsicher sind, indem Sie einige der oben genannten Schritte ausführen. Wenn es darum geht, nehmen andere Edelsteine ​​an, dass sie nicht threadsicher sind, es sei denn, sie sagen, dass sie es sind, und wenn sie sagen, dass sie es nicht sind, und schauen Sie sich ihren Code an (aber nur, weil Sie sehen, dass sie Dinge wie gehen@n ||= 1 bedeutet nicht, dass sie nicht threadsicher sind, das ist eine absolut legitime Sache, die im richtigen Kontext zu tun ist. Sie sollten stattdessen nach Dingen wie dem veränderlichen Status in globalen Variablen suchen, wie sie mit veränderlichen Objekten umgehen, die an ihre Methoden übergeben werden, und insbesondere wie sie behandelt Options-Hashes).

Schließlich ist es eine transitive Eigenschaft, Thread-unsicher zu sein. Alles, was etwas verwendet, das nicht threadsicher ist, ist selbst nicht threadsicher.

Das Ö
quelle
Gute Antwort. Angesichts der Tatsache, dass eine typische Rails-App mehrere Prozesse umfasst (wie Sie beschrieben haben, greifen viele verschiedene Benutzer auf dieselbe App zu), frage ich mich , wie hoch das marginale Risiko von Threads für das Parallelitätsmodell ist ... Mit anderen Worten, wie viel "gefährlicher" soll es im Thread-Modus ausgeführt werden, wenn Sie bereits über Prozesse mit Parallelität zu tun haben?
Ingwerlime
2
@ Theo Vielen Dank. Das ständige Zeug ist eine große Bombe. Es ist nicht einmal prozesssicher. Wenn die Konstante in einer Anforderung geändert wird, werden die späteren Anforderungen die geänderte Konstante auch in einem einzelnen Thread anzeigen.
Rubinkonstanten
5
Tun STANDARD_OPTIONS = {...}.freeze, um auf flachen Mutationen zu erhöhen
Glebm
Wirklich tolle Antwort
Cheyne
3
"Wenn Sie Code wie @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[...] schreiben , ist der Wert der gemeinsam genutzten Variablen danach nicht deterministisch." - Wissen Sie, ob dies zwischen Ruby-Versionen unterschiedlich ist? Zum Beispiel ergibt das Ausführen Ihres Codes auf 1.8 unterschiedliche Werte von @n, aber auf 1.9 und höher scheint es konsistent @ngleich 300 zu sein.
user200783
10

Zusätzlich zu Theos Antwort möchte ich einige Problembereiche hinzufügen, auf die Sie in Rails besonders achten sollten, wenn Sie zu config.threadsafe wechseln!

  • Klassenvariablen :

    @@i_exist_across_threads

  • ENV :

    ENV['DONT_CHANGE_ME']

  • Themen :

    Thread.start

crizCraig
quelle
9

Ab Rails 4 müsste standardmäßig alles in einer Thread-Umgebung ausgeführt werden

Dies ist nicht 100% korrekt. Thread-sichere Schienen sind standardmäßig aktiviert. Wenn Sie auf einem App-Server mit mehreren Prozessen wie Passenger (Community) oder Unicorn bereitstellen, gibt es überhaupt keinen Unterschied. Diese Änderung betrifft Sie nur, wenn Sie in einer Multithread-Umgebung wie Puma oder Passenger Enterprise> 4.0 bereitstellen

Wenn Sie in der Vergangenheit auf einem Multithread-App-Server bereitstellen wollten, mussten Sie config.threadsafe aktivieren , was jetzt Standard ist, da alles entweder keine Auswirkungen hatte oder auch auf eine Rails-App angewendet wurde, die in einem einzigen Prozess ausgeführt wurde ( Prooflink ).

Wenn Sie jedoch alle Rails 4- Streaming- Vorteile und andere Echtzeitfunktionen der Multithread-Bereitstellung nutzen möchten , ist dieser Artikel möglicherweise interessant. Wie @Theo traurig ist, müssen Sie für eine Rails-App während einer Anforderung lediglich den mutierenden statischen Status weglassen. Obwohl dies eine einfache Übung ist, können Sie sich leider nicht für jeden Edelstein, den Sie finden, sicher sein. Soweit ich mich erinnere, hatte Charles Oliver Nutter vom JRuby-Projekt in diesem Podcast einige Tipps dazu .

Und wenn Sie eine reine gleichzeitige Ruby-Programmierung schreiben möchten, bei der Sie einige Datenstrukturen benötigen, auf die mehr als ein Thread zugreift, ist das Juwel thread_safe möglicherweise hilfreich.

dre-hh
quelle