Keine der Kerndatenstrukturen sind Thread-sicher. Der einzige, den ich kenne, der mit Ruby ausgeliefert wird, ist die Queue-Implementierung in der Standard-Bibliothek (require 'thread'; q = Queue.new
).
MRI's GIL rettet uns nicht vor Threadsicherheitsproblemen. Es stellt nur sicher, dass zwei Threads Ruby-Code zur gleichen Zeit, d. H. Auf zwei verschiedenen CPUs zur gleichen Zeit nicht ausführen können. Threads können an einem beliebigen Punkt in Ihrem Code pausiert und fortgesetzt werden. Wenn Sie Code wie @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }
schreiben, z. Wenn eine freigegebene Variable aus mehreren Threads mutiert wird, ist der Wert der gemeinsam genutzten Variablen danach nicht deterministisch. Die GIL ist mehr oder weniger eine Simulation eines einzelnen Kernsystems, sie ändert nicht die grundlegenden Probleme beim Schreiben korrekter konkurrierender Programme.
Selbst wenn die MRI wie Node.js single-threaded gewesen wäre, müssten Sie immer noch über Nebenläufigkeit nachdenken. Das Beispiel mit der inkrementierten Variablen würde gut funktionieren, aber Sie können immer noch Race-Bedingungen erhalten, bei denen Dinge in nicht-deterministischer Reihenfolge passieren und ein Callback das Ergebnis eines anderen Clobbers überlagert. Asynchrone Single-Threaded-Systeme sind einfacher zu verstehen, aber sie sind nicht frei von Nebenläufigkeitsproblemen. Denken Sie nur an eine Anwendung mit mehreren Benutzern: Wenn zwei Benutzer gleichzeitig in einem Stapelüberlauf-Post bearbeiten, verbringen Sie etwas Zeit mit dem Bearbeiten des Posts und drücken Sie dann auf Speichern, dessen Änderungen später von einem dritten Benutzer gesehen werden Lesen Sie den gleichen Beitrag?
In Ruby, wie in den meisten anderen gleichzeitigen Laufzeiten, ist alles, was mehr als eine Operation ist, nicht Thread-sicher. @n += 1
ist nicht threadsicher, da es sich um mehrere Operationen handelt. @n = 1
ist Thread-sicher, weil es eine Operation ist (es gibt viele Operationen unter der Haube, und ich würde wahrscheinlich in Schwierigkeiten geraten, wenn ich versuchen würde zu beschreiben, warum es "thread-safe" im Detail ist, aber am Ende werden Sie keine inkonsistenten Ergebnisse erhalten Zuordnungen). @n ||= 1
, ist nicht und keine andere Kurzschrift Operation + Zuweisung ist entweder. Ein Fehler, den ich oft gemacht habe, ist return unless @started; @started = true
zu schreiben, was überhaupt nicht threadsicher ist.
Ich kenne keine autoritative Liste von threadsicheren und nicht threadsicheren Anweisungen für Ruby, aber es gibt eine einfache Faustregel: Wenn ein Ausdruck nur eine (side-effect free) Operation ausführt, ist es wahrscheinlich fadensicher.Zum Beispiel: a + b
ok ist, a = b
auch in Ordnung ist, und a.foo(b)
ist in Ordnung, , wenn die Methode foo
ist nebenwirkungsfreie (da gerade über alles in Ruby ein Methodenaufruf ist, auch in vielen Fällen Zuordnung, geht dies für die andere Beispiele auch). Nebenwirkungen bedeutet in diesem Zusammenhang Dinge, die den Zustand ändern. def foo(x); @x = x; end
ist nicht Nebenwirkung frei.
Eines der schwierigsten Dinge beim Schreiben von threadsicherem Code in Ruby ist, dass alle Kerndatenstrukturen, einschließlich Array, Hash und String, änderbar sind. Es ist sehr leicht, versehentlich ein Stück deines Zustands zu verlieren, und wenn dieses Stück veränderbar ist, können die Dinge wirklich vermasselt werden. Betrachten Sie den folgenden Code ein:
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 zwischen Threads gemeinsam genutzt werden, und sie können sicher hinzufügen Dinge, aber es gibt einen gemeinsamen Zugriff Fehler (es ist nicht der einzige): der interne Zustand des Objekts Lecks durch den stuff
Accessor. Abgesehen davon, dass es aus der Verkapselungsperspektive problematisch ist, öffnet es auch eine Dose von Parallelwürmern. Vielleicht nimmt jemand dieses Array und gibt es an einen anderen Ort weiter, und dieser Code wiederum glaubt, dass er jetzt dieses Array besitzt und damit machen kann, was er will.
Ein weiteres klassisches Ruby-Beispiel ist dies:
STANDARD_OPTIONS = {:color => 'red', :count => 10}
def find_stuff
@some_service.load_things('stuff', STANDARD_OPTIONS)
end
find_stuff
Werke zum ersten Mal eine Fein es verwendet wird, liefert aber etwas anderes das zweite Mal. Warum? Die Methode load_things
geht davon aus, dass sie den übergebenen Options-Hash besitzt und color = options.delete(:color)
. Jetzt hat die Konstante STANDARD_OPTIONS
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. Denken Sie nur darüber nach, was passieren würde, wenn dieser Code gleichzeitig ausgeführt würde.
Wenn Sie den gemeinsamen veränderbaren Zustand vermeiden (z. B. Instanzvariablen in Objekten, auf die mehrere Threads zugreifen, Datenstrukturen wie Hashes und Arrays, auf die mehrere Threads zugreifen), ist die Threadsicherheit 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 ein neues Controller-Objekt für jede Anfrage erstellt, so dass es nur von einem einzelnen Thread verwendet wird, und dasselbe gilt für alle Modellobjekte, die Sie von diesem Controller erstellen. Rails ermutigt jedoch auch die Verwendung globaler Variablen (User.find(...)
verwendet die globale Variable User
, Sie können es sich 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 nur gelesen werden, aber manchmal speichern Sie Dinge in diesen globalen Variablen, weil es bequem ist. Sei vorsichtig, wenn du etwas verwendest, das global zugänglich ist.
Es war schon eine ganze Weile möglich, Rails in Threaded Umgebungen laufen zu lassen, also ohne ein Rails-Experte zu sein, würde ich immer noch sagen, dass man sich bei Rails nicht um Thread-Sicherheit kümmern muss selbst. Sie können immer noch Rails-Anwendungen erstellen, die nicht Thread-sicher sind, indem Sie einige der oben erwähnten Dinge tun. Wenn es darum geht andere Edelsteine anzunehmen, dass sie nicht threadsicher sind, es sei denn sie sagen, dass sie es sind, und wenn sie sagen, dass sie davon ausgehen, dass sie es nicht sind, und ihren Code durchsehen (aber nur weil man sieht, dass sie Dinge wie @n ||= 1
tun nicht bedeuten, dass sie nicht threadsicher sind, das ist eine vollkommen legitime Sache im richtigen Kontext - Sie sollten stattdessen nach Dingen wie veränderbaren Status in globalen Variablen suchen, wie sie änderbare Objekte behandelt, die an ihre Methoden übergeben werden, und vor allem wie sie behandelt Optionen Hashes).
Schließlich ist Threads unsicher eine transitive Eigenschaft. Alles, was etwas verwendet, das nicht threadsicher ist, ist selbst nicht threadsicher.
Sind Sie sicher, dass der gesamte Code und alle Edelsteine müssen threadsicher sein? Was die Release Notes sagen, ist, dass Rails selbst threadsicher sein wird, nicht dass alles, was damit verwendet wird, – enthrops
ist. Multithread-Tests wären das schlimmstmögliche Threadsafe-Risiko. Wenn Sie den Wert einer Umgebungsvariablen um Ihren Testfall herum ändern müssen, sind Sie nicht sofort threadsafe. Wie würdest du das umgehen? Und ja, alle Edelsteine müssen threadsicher sein. –