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.