class Ractor
Ractor.new erstellt einen neuen Ractor, der parallel zu anderen Ractoren laufen kann.
# The simplest ractor r = Ractor.new {puts "I am in Ractor!"} r.join # wait for it to finish # Here, "I am in Ractor!" is printed
Raktoren teilen nicht alle Objekte miteinander. Dies hat zwei Hauptvorteile: Datenwettläufe und Wettlaufsituationen, wie sie bei der Thread-Sicherheit auftreten, sind zwischen Raktoren nicht möglich. Der andere Vorteil ist die Parallelität.
Um dies zu erreichen, ist die Objektteilung zwischen Raktoren eingeschränkt. Im Gegensatz zu Threads können Raktoren nicht auf alle in anderen Raktoren verfügbaren Objekte zugreifen. Zum Beispiel ist die Nutzung von Objekten, die normalerweise über Variablen im äußeren Geltungsbereich verfügbar sind, zwischen Raktoren verboten.
a = 1 r = Ractor.new {puts "I am in Ractor! a=#{a}"} # fails immediately with # ArgumentError (can not isolate a Proc because it accesses outer variables (a).)
Das Objekt muss explizit geteilt werden
a = 1 r = Ractor.new(a) { |a1| puts "I am in Ractor! a=#{a1}"}
Auf CRuby (der Standardimplementierung) wird der Global Virtual Machine Lock (GVL) pro Ractor gehalten, sodass Raktoren parallel laufen können. Dies steht im Gegensatz zur Situation bei Threads auf CRuby.
Anstatt auf gemeinsam genutzten Zustand zuzugreifen, sollten Objekte zwischen Raktoren übermittelt werden, indem sie als Nachrichten gesendet und empfangen werden.
a = 1 r = Ractor.new do a_in_ractor = receive # receive blocks the Thread until our default port gets sent a message puts "I am in Ractor! a=#{a_in_ractor}" end r.send(a) # pass it r.join # Here, "I am in Ractor! a=1" is printed
Darüber hinaus werden alle Argumente, die an Ractor.new übergeben werden, an den Block weitergegeben und sind dort verfügbar, als ob sie von Ractor.receive empfangen worden wären. Der letzte Blockwert kann mit Ractor#value empfangen werden.
Teilbare und nicht teilbare Objekte
Wenn ein Objekt an einen Ractor gesendet wird, ist es wichtig zu verstehen, ob das Objekt teilbar oder nicht teilbar ist. Die meisten Ruby-Objekte sind nicht teilbar. Selbst gefrorene Objekte können nicht teilbar sein, wenn sie (durch ihre Instanzvariablen) nicht gefrorene Objekte enthalten.
Teilbare Objekte sind solche, die von mehreren Raktoren gleichzeitig verwendet werden können, ohne die Thread-Sicherheit zu gefährden, z. B. Zahlen, true und false. Ractor.shareable? ermöglicht die Überprüfung, und Ractor.make_shareable versucht, das Objekt teilbar zu machen, wenn es dies nicht bereits ist, und gibt einen Fehler aus, wenn dies nicht gelingt.
Ractor.shareable?(1) #=> true -- numbers and other immutable basic values are shareable Ractor.shareable?('foo') #=> false, unless the string is frozen due to # frozen_string_literal: true Ractor.shareable?('foo'.freeze) #=> true Ractor.shareable?([Object.new].freeze) #=> false, inner object is unfrozen ary = ['hello', 'world'] ary.frozen? #=> false ary[0].frozen? #=> false Ractor.make_shareable(ary) ary.frozen? #=> true ary[0].frozen? #=> true ary[1].frozen? #=> true
Wenn ein teilbares Objekt über send gesendet wird, erfolgt keine zusätzliche Verarbeitung und es wird für beide Raktoren nutzbar. Wenn ein nicht teilbares Objekt gesendet wird, kann es entweder *kopiert* oder *verschoben* werden. Kopieren ist der Standard; dabei wird das Objekt vollständig durch Deep Cloning (Object#clone) der nicht teilbaren Teile seiner Struktur kopiert.
data = ['foo'.dup, 'bar'.freeze] r = Ractor.new do data2 = Ractor.receive puts "In ractor: #{data2.object_id}, #{data2[0].object_id}, #{data2[1].object_id}" end r.send(data) r.join puts "Outside : #{data.object_id}, #{data[0].object_id}, #{data[1].object_id}"
Dies gibt etwas aus wie
In ractor: 8, 16, 24 Outside : 32, 40, 24
Beachten Sie, dass sich die Objekt-IDs des Arrays und der nicht gefrorene String innerhalb des Arrays im Ractor geändert haben, da es sich um verschiedene Objekte handelt. Das Element des zweiten Arrays, ein teilbarer gefrorener String, ist dasselbe Objekt.
Das Deep Cloning von Objekten kann langsam und manchmal unmöglich sein. Alternativ kann move: true beim Senden verwendet werden. Dies *verschiebt* das nicht teilbare Objekt in den empfangenden Ractor, sodass es für den sendenden Ractor unzugänglich wird.
data = ['foo', 'bar'] r = Ractor.new do data_in_ractor = Ractor.receive puts "In ractor: #{data_in_ractor.object_id}, #{data_in_ractor[0].object_id}" end r.send(data, move: true) r.join puts "Outside: moved? #{Ractor::MovedObject === data}" puts "Outside: #{data.inspect}"
Dies gibt aus
In ractor: 100, 120 Outside: moved? true test.rb:9:in `method_missing': can not send any methods to a moved object (Ractor::MovedError)
Beachten Sie, dass selbst inspect und grundlegendere Methoden wie __id__ bei einem verschobenen Objekt nicht zugänglich sind.
Klassen und Module-Objekte sind teilbar und ihre Klassen-/Moduldefinitionen werden zwischen Raktoren geteilt. Ractor-Objekte sind ebenfalls teilbar. Alle Operationen auf teilbaren Objekten sind über Raktoren hinweg Thread-sicher. Die Definition von veränderlichen, teilbaren Objekten in Ruby ist nicht möglich, aber C-Erweiterungen können sie einführen.
Der Zugriff (Abrufen) auf Instanzvariablen von teilbaren Objekten in anderen Raktoren ist untersagt, wenn die Werte der Variablen nicht teilbar sind. Dies kann vorkommen, da Module/Klassen teilbar sind, aber Instanzvariablen enthalten können, deren Werte nicht teilbar sind. In Nicht-Main-Raktoren ist es auch untersagt, Instanzvariablen von Klassen/Modulen zu setzen (auch wenn der Wert teilbar ist).
class C class << self attr_accessor :tricky end end C.tricky = "unshareable".dup r = Ractor.new(C) do |cls| puts "I see #{cls}" puts "I can't see #{cls.tricky}" cls.tricky = true # doesn't get here, but this would also raise an error end r.join # I see C # can not access instance variables of classes/modules from non-main Ractors (RuntimeError)
Raktoren können auf Konstanten zugreifen, wenn diese teilbar sind. Der Haupt-Ractor ist der einzige, der auf nicht teilbare Konstanten zugreifen kann.
GOOD = 'good'.freeze BAD = 'bad'.dup r = Ractor.new do puts "GOOD=#{GOOD}" puts "BAD=#{BAD}" end r.join # GOOD=good # can not access non-shareable objects in constant Object::BAD by non-main Ractor. (NameError) # Consider the same C class from above r = Ractor.new do puts "I see #{C}" puts "I can't see #{C.tricky}" end r.join # I see C # can not access instance variables of classes/modules from non-main Ractors (RuntimeError)
Siehe auch die Beschreibung der Pragmatik # shareable_constant_value in der Erklärung der Kommentarsyntax.
Raktoren vs. Threads
Jeder Ractor hat seinen eigenen Haupt-Thread. Neue Threads können aus Raktoren heraus erstellt werden (und auf CRuby teilen sie den GVL mit anderen Threads dieses Ractors).
r = Ractor.new do a = 1 Thread.new {puts "Thread in ractor: a=#{a}"}.join end r.join # Here "Thread in ractor: a=1" will be printed
Hinweis zu Codebeispielen
In den folgenden Beispielen verwenden wir manchmal die folgende Methode, um auf den Fortschritt oder das Ende von Raktoren zu warten.
def wait sleep(0.1) end
Dies dient **nur zu Demonstrationszwecken** und sollte nicht im echten Code verwendet werden. Meistens wird join verwendet, um auf das Ende von Raktoren zu warten, und Ractor.receive wird verwendet, um auf Nachrichten zu warten.
Referenz
Siehe Ractor Design Doc für weitere Details.
Öffentliche Klassenmethoden
Source
# File ractor.rb, line 490 def self.[](sym) Primitive.ractor_local_value(sym) end
Ruft einen Wert aus dem Ractor-lokalen Speicher für den aktuellen Ractor ab.
Source
# File ractor.rb, line 495 def self.[]=(sym, val) Primitive.ractor_local_value_set(sym, val) end
Setzt einen Wert im Ractor-lokalen Speicher für den aktuellen Ractor.
Source
# File ractor.rb, line 258 def self.count __builtin_cexpr! %q{ ULONG2NUM(GET_VM()->ractor.cnt); } end
Gibt die Anzahl der derzeit laufenden oder blockierenden (wartenden) Raktoren zurück.
Ractor.count #=> 1 r = Ractor.new(name: 'example') { Ractor.receive } Ractor.count #=> 2 (main + example ractor) r << 42 # r's Ractor.receive will resume r.join # wait for r's termination Ractor.count #=> 1
Source
# File ractor.rb, line 244 def self.current __builtin_cexpr! %q{ rb_ractor_self(rb_ec_ractor_ptr(ec)); } end
Gibt den aktuell ausgeführten Ractor zurück.
Ractor.current #=> #<Ractor:#1 running>
Source
# File ractor.rb, line 519 def self.main __builtin_cexpr! %q{ rb_ractor_self(GET_VM()->ractor.main_ractor); } end
Gibt den Haupt-Ractor zurück.
Source
# File ractor.rb, line 526 def self.main? __builtin_cexpr! %q{ RBOOL(GET_VM()->ractor.main_ractor == rb_ec_ractor_ptr(ec)) } end
Gibt true zurück, wenn der aktuelle Ractor der Haupt-Ractor ist.
Source
# File ractor.rb, line 229 def self.new(*args, name: nil, &block) b = block # TODO: builtin bug raise ArgumentError, "must be called with a block" unless block if __builtin_cexpr!("RBOOL(ruby_single_main_ractor)") Kernel.warn("Ractor API is experimental and may change in future versions of Ruby.", uplevel: 0, category: :experimental) end loc = caller_locations(1, 1).first loc = "#{loc.path}:#{loc.lineno}" __builtin_ractor_create(loc, name, args, b) end
Erstellt einen neuen Ractor mit Argumenten und einem Block.
Der gegebene Block (Proc) ist isoliert (kann nicht auf äußere Variablen zugreifen). self innerhalb des Blocks bezieht sich auf den aktuellen Ractor.
r = Ractor.new { puts "Hi, I am #{self.inspect}" } r.join # Prints "Hi, I am #<Ractor:#2 test.rb:1 running>"
Alle übergebenen args werden gemäß denselben Regeln wie per send/Ractor.receive gesendete Objekte an die Blockargumente weitergegeben. Wenn ein Argument in args nicht teilbar ist, wird es kopiert (durch Deep Cloning, was ineffizient sein kann).
arg = [1, 2, 3] puts "Passing: #{arg} (##{arg.object_id})" r = Ractor.new(arg) {|received_arg| puts "Received: #{received_arg} (##{received_arg.object_id})" } r.join # Prints: # Passing: [1, 2, 3] (#280) # Received: [1, 2, 3] (#300)
Der name eines Ractors kann zu Debugging-Zwecken gesetzt werden
r = Ractor.new(name: 'my ractor') {}; r.join p r #=> #<Ractor:#3 my ractor test.rb:1 terminated>
Source
# File ractor.rb, line 349 def self.receive Ractor.current.default_port.receive end
Empfängt eine Nachricht vom Standardport des aktuellen Ractors.
Source
# File ractor.rb, line 308 def self.select(*ports) raise ArgumentError, 'specify at least one Ractor::Port or Ractor' if ports.empty? monitors = {} # Ractor::Port => Ractor ports = ports.map do |arg| case arg when Ractor port = Ractor::Port.new monitors[port] = arg arg.monitor port port when Ractor::Port arg else raise ArgumentError, "should be Ractor::Port or Ractor" end end begin result_port, obj = __builtin_ractor_select_internal(ports) if r = monitors[result_port] [r, r.value] else [result_port, obj] end ensure # close all ports for join monitors.each do |port, r| r.unmonitor port port.close end end end
Blockiert den aktuellen Thread, bis einer der gegebenen Ports eine Nachricht empfangen hat. Gibt ein Array mit zwei Elementen zurück, wobei das erste Element der Port und das zweite das empfangene Objekt ist. Diese Methode kann auch Ractor-Objekte selbst akzeptieren, und in diesem Fall wartet sie, bis einer beendet wurde, und gibt ein zweielementiges Array zurück, wobei das erste Element der Ractor und das zweite sein Abbruchwert ist.
p1, p2 = Ractor::Port.new, Ractor::Port.new ps = [p1, p2] rs = 2.times.map do |i| Ractor.new(ps.shift, i) do |p, i| sleep rand(0.99) p.send("r#{i}") sleep rand(0.99) "r#{i} done" end end waiting_on = [p1, p2, *rs] until waiting_on.empty? received_on, obj = Ractor.select(*waiting_on) waiting_on.delete(received_on) puts obj end # r0 # r1 # r1 done # r0 done
Das folgende Beispiel ist fast äquivalent zu ractors.map(&:value), außer dass der Thread entblockiert wird, sobald einer der Raktoren beendet ist, anstatt auf deren Beendigung in der Reihenfolge der Array-Elemente zu warten.
values = [] until ractors.empty? r, val = Ractor.select(*ractors) ractors.delete(r) values << val end
Source
# File ractor.rb, line 513 def self.store_if_absent(sym) Primitive.attr! :use_block Primitive.ractor_local_value_store_if_absent(sym) end
Wenn der entsprechende Ractor-lokale Wert nicht gesetzt ist, wird ein Wert mit init_block übergeben und der Wert auf Thread-sichere Weise gespeichert. Diese Methode gibt den gespeicherten Wert zurück.
(1..10).map{ Thread.new(it){|i| Ractor.store_if_absent(:s){ f(); i } #=> return stored value of key :s } }.map(&:value).uniq.size #=> 1 and f() is called only once
Öffentliche Instanzmethoden
Source
# File ractor.rb, line 473 def [](sym) if (self != Ractor.current) raise RuntimeError, "Cannot get ractor local storage for non-current ractor" end Primitive.ractor_local_value(sym) end
Source
# File ractor.rb, line 482 def []=(sym, val) if (self != Ractor.current) raise RuntimeError, "Cannot set ractor local storage for non-current ractor" end Primitive.ractor_local_value_set(sym, val) end
Setzt einen Wert im Ractor-lokalen Speicher für den aktuellen Ractor. Veraltet, verwenden Sie stattdessen Ractor.[]=.
Source
# File ractor.rb, line 403 def close default_port.close end
Schließt den Standardport. Das Schließen eines Ports ist nur dem Ractor gestattet, der den Port erstellt hat. Daher muss der Empfänger der aktuelle Ractor sein.
Source
# File ractor.rb, line 566 def default_port __builtin_cexpr! %q{ ractor_default_port_value(RACTOR_PTR(self)) } end
Gibt den Standardport des Ractor zurück.
Source
# File ractor.rb, line 374 def inspect loc = __builtin_cexpr! %q{ RACTOR_PTR(self)->loc } name = __builtin_cexpr! %q{ RACTOR_PTR(self)->name } id = __builtin_cexpr! %q{ UINT2NUM(rb_ractor_id(RACTOR_PTR(self))) } status = __builtin_cexpr! %q{ rb_str_new2(ractor_status_str(RACTOR_PTR(self)->status_)) } "#<Ractor:##{id}#{name ? ' '+name : ''}#{loc ? " " + loc : ''} #{status}>" end
Source
# File ractor.rb, line 585 def join port = Port.new self.monitor port if port.receive == :aborted __builtin_ractor_value end self ensure port.close end
Source
# File ractor.rb, line 634 def monitor port __builtin_ractor_monitor(port) end
Registriert den Port als Überwachungsport für diesen Ractor. Wenn der Ractor beendet wird, empfängt der Port ein Symbol-Objekt.
-
:exitedwird gesendet, wenn der Ractor ohne eine unbehandelte Ausnahme beendet wird. -
:abortedwird gesendet, wenn der Ractor durch eine unbehandelte Ausnahme beendet wird.r = Ractor.new{ some_task() } r.monitor(port = Ractor::Port.new) port.receive #=> :exited and r is terminated r = Ractor.new{ raise "foo" } r.monitor(port = Ractor::Port.new) port.receive #=> :aborted and r is terminated by the RuntimeError "foo"
Source
# File ractor.rb, line 387 def name __builtin_cexpr! %q{RACTOR_PTR(self)->name} end
Gibt den in Ractor.new gesetzten Namen zurück oder nil.
Source
# File ractor.rb, line 368 def send(...) default_port.send(...) self end
Dies entspricht Port#send an den default_port des Ractors.
Source
# File ractor.rb, line 644 def unmonitor port __builtin_ractor_unmonitor(port) end
Hebt die Registrierung des Ports von den Überwachungsports für diesen Ractor auf.
Source
# File ractor.rb, line 611 def value self.join __builtin_ractor_value end
Wartet auf die Fertigstellung des ractor und gibt dessen Wert zurück oder löst die Ausnahme aus, die den Ractor beendet hat. Der Abbruchwert wird in den aufrufenden Ractor verschoben. Daher kann höchstens 1 Ractor den Abbruchwert eines anderen Ractors empfangen.
r = Ractor.new{ [1, 2] } r.value #=> [1, 2] (unshareable object) Ractor.new(r){|r| r.value} #=> Ractor::Error
Private Instanzmethoden
Source
# File ractor.rb, line 358 def receive default_port.receive end
gleich wie Ractor.receive