Dieser Artikel richtet sich an diejenigen, die an der in Ruby3 eingeführten neuen Funktion "Ractor" für die parallele / parallele Verarbeitung interessiert sind. Während ich den Beispielcode mit einem einfachen "Ractor" erklärte, schrieb ich ihn, damit ich mein Verständnis vertiefen kann (obwohl es den Anschein hat, dass es in Zukunft ein Artikel für mich ist ...).
Es ist auch ein Artikel, der die Ergebnisse der Untersuchung zusammenfasst, was mit "Ractor" selbst getan werden kann. In der zweiten Hälfte wird daher das Verhalten anhand des Codes beim Spielen mit "Ractor" erläutert.
Ich bin mir sicher, dass einige Leute überhaupt nichts über Ractor
wissen, also werde ich es kurz vorstellen.
Ractor
ist eine neue Funktion für die parallele / parallele Verarbeitung, die in Ruby3 eingeführt wurde. Das Feature selbst wird seit mehreren Jahren unter dem Namen "Guild" vorgeschlagen.
Es gab jedoch eine Stimme aus der Spielebranche, die sagte: "Ich verwende den Namen Guild, also möchte ich, dass du einen anderen Namen verwendest", und sie wurde in den aktuellen "Ractor" geändert.
Es scheint, dass es sich um das "Schauspieler" -Modell handelt, also scheint es, dass es in "Ractor (Rubys Schauspieler)" umbenannt wurde.
Ractor
ist eine Einheit der parallelen Ausführung, und jede wird parallel ausgeführt.
Im folgenden Code werden beispielsweise "Puts: Hallo" und "Puts: Hallo" parallel ausgeführt.
Ractor.new do
5.times do
puts :hello
end
end
5.times do
puts :world
end
Das Ausführen dieses Codes führt zu folgendem Ergebnis:
world
helloworld
hello
world
helloworld
helloworld
hello
Auf diese Weise kann jeder Prozess parallel ausgeführt werden.
Der "Ractor" kann auch Objekte an einen anderen "Ractor" senden, empfangen und synchron ausführen. Es gibt zwei Synchronisationsmethoden, "Push-Typ" und "Pull-Typ".
Im Fall von "Push-Typ" lautet der Code beispielsweise wie folgt.
r1 = Ractor.new do
:hoge
end
r2 = Ractor.new do
puts :fuga, Ractor.recv
end
r2.send(r1.take)
r2.take
# => :fuga, :hoge
Mit Ractor
können Sie die send
-Methode verwenden, um ein Objekt an einen anderen Ractor
zu senden. Mit dem obigen Code
r2.send(r1.take)
Es wird im Teil von an "r2" gesendet.
Übermittelte Objekte können in Ractor
mit Ractor.recv
empfangen werden
r2 = Ractor.new do
puts :fuga, Ractor.recv #
end
Sie können das von "r2.send (r1.take)" gesendete Objekt nehmen und an die "Puts" -Methode übergeben.
Es verwendet auch die "take" -Methode, um das Ergebnis der Ausführung des "Ractor" zu erhalten.
Also empfängt r1.take``: hoge
.
Mit anderen Worten, "r2.send (r1.take)" empfängt das Ausführungsergebnis von "r1" und sendet es an "r2". Und "setzt: fuga, Ractor.recv" in "r2" wird zu "setzt: fuga ,: hoge", was bedeutet, dass "fuga" bzw. "hoge" ausgegeben werden.
Dies ist der Fluss des Austauschs von Objekten mit "Push-Typ".
Auf der anderen Seite hat "Pull-Typ" den folgenden Code.
r1 = Ractor.new 42 do |arg|
Ractor.yield arg
end
r2 = Ractor.new r1 do |r1|
r1.take
end
puts r2.take
Ractor.new
Das Argument ging ein|arg|
Sie können es als Variable empfangen, die im Block wie verwendet werden kann.
Der folgende Code wartet beispielsweise darauf, dass "r1" die Methode "take" ausführt.
r1 = Ractor.new 42 do |arg|
Ractor.yield arg
end
Sie können auch einen anderen Ractor
an Ractor.new
übergeben, damit Sie schreiben können:
r2 = Ractor.new r1 do |r1|
r1.take
end
Jetzt können Sie die "42" erhalten, die "r1" als Argument in "r2" erhalten hat.
Schließlich empfängt und gibt setzt r2.take`` 42
aus.
Der "Pull-Typ" ist so.
Grob erklären
--push type
: Ractor # send
+ Ractor.recv
--pull type
: Ractor.yield
+ Ractor # take
Es ist wie es ist.
Eine ausführlichere Erklärung von Ractor
finden Sie unter dem folgenden Link.
Ractor
ist Ractor.new
und schreibt den Prozess, den Sie ausführen möchten, in den Block.
Ractor.new do
#Dieser Block läuft parallel
end
Die Verarbeitung in diesem Block wird parallel ausgeführt.
Mit anderen Worten, im Fall des folgenden Codes
Ractor.new do
10.times do
puts :hoge
end
end
10.times do
puts :fuga
end
: hoge
und: fuga
werden parallel ausgegeben.
Da der Prozess, den Sie ausführen möchten, als Block an "Ractor.new" übergeben wird, können Sie auch wie folgt schreiben.
Ractor.new{
10.times{
puts :hoge
}
}
Sie können es auch mit dem Schlüsselwortargument name
benennen und den Namen auch mit Ractor # name
erhalten.
r = Ractor.new name: 'r1' do
puts :hoge
end
p r.name
# => "r1"
Auf diese Weise können Sie auch sehen, welcher Ractor den Vorgang ausführt.
Sie können ein Objekt innerhalb eines Blocks übergeben, indem Sie ein Argument an Ractor.new
übergeben.
r = Ractor.new :hoge do |a|
p a
end
r.take
# => :hoge
Auf diese Weise können Sie Objekte über Argumente übergeben.
Sie können auch mehrere Argumente übergeben
r = Ractor.new :hoge, :fuga do |a, b|
p a
p b
end
r.take
# => fuga
# => hoge
Sie können "Array" auch so übergeben.
r = Ractor.new [:hoge, :fuga] do |a|
p a.inspect
end
r.take
# => "[:hoge, :fuga]"
Apropos,|a|
Zu|a, b|
Wenn Sie zu wechseln
r = Ractor.new [:hoge, :fuga] do |a, b|
p a
p b
end
r.take
# => :hoge
# => :fuga
Das Ausgabeergebnis wird sein. Dies scheint als dasselbe Verhalten wie "a, b = [: hoge,: fuga]" interpretiert zu werden.
Auch im Fall von "Hash"
r = Ractor.new({:hoge => 42, :fuga => 21}) do |a|
p a
p a[:hoge]
end
r.take
# => {:hoge=>42, :fuga=>21}
# => 42
Wird ausgegeben.
Übrigens, wenn Sie es nach Ractor.new
nicht in()
einschließen, erhalten Sie SyntaxError
, seien Sie also vorsichtig.
r = Ractor.new({:hoge => 42, :fuga => 21}) do |a|
p a
p a[:hoge]
end
r.take
# => SyntaxError
Ractor kann den Rückgabewert im ausgeführten Block mit der Methode "take" empfangen.
r = Ractor.new do
:hoge
end
p r.take
# => :hoge
Übrigens, wenn Sie im Block "return" machen, scheint es "LocalJumpError" zu sein.
r = Ractor.new do
return :fuga
:hoge
end
p r.take
# => LocalJumpError
Ausnahmen innerhalb von Ractor können wie folgt empfangen werden:
r = Ractor.new do
raise 'error'
end
begin
r.take
rescue Ractor::RemoteError => e
p e.message
end
Übrigens können Sie auch die Ausnahmebehandlung in Ractor schreiben.
r = Ractor.new name: 'r1' do
begin
raise 'error'
rescue => e
p e.message
end
end
r.take
Der Dokumentation zufolge können Sie außerdem Ausnahmen in dem Bereich abfangen, in dem der vom Ractor-Block zurückgegebene Wert empfangen wird. Mit anderen Worten, Sie können auch den folgenden Code schreiben.
r1 = Ractor.new do
raise 'error'
end
r2 = Ractor.new r1 do |r1|
begin
r1.take
rescue Ractor::RemoteError => e
p e.message
end
end
r2.take
# => "thrown by remote Ractor."
Sie können so parallel zu Ractor ausführen.
Ractor.new do
3.times do
puts 42
end
end
3.times do
puts 21
end
Bei der Ausführung werden die Ausgänge "42" und "21" separat angezeigt.
Sie können mit "Ractor" mehrere "Worker" generieren, diese über "Pipe" übergeben, Werte übergeben und die Ergebnisse wie unten gezeigt zusammenfassen.
require 'prime'
pipe = Ractor.new do
loop do
Ractor.yield Ractor.recv
end
end
N = 1000
RN = 10
workers = (1..RN).map do
Ractor.new pipe do |pipe|
while n = pipe.take
Ractor.yield [n, n.prime?]
end
end
end
(1..N).each{|i|
pipe << i
}
pp (1..N).map{
r, (n, b) = Ractor.select(*workers)
[n, b]
}.sort_by{|(n, b)| n}
# => 0 ~Gibt das Ergebnis aus, ob Zahlen bis 999 Primzahlen sind
Dieser Code erstellt 10 "Arbeiter" und übergibt jedem "Arbeiter" ein Objekt über "Pipe". Es gibt auch das empfangene Objekt mit "Ractor.yield [n, n.prime?]" Zurück.
Sie können mehrere "Worker" wie diese erstellen, sie über "Pipe" verarbeiten und die Ergebnisse erhalten.
Mit dem vorherigen Code wurde die Verarbeitung in "Worker" wahrscheinlich später groß, daher habe ich eine Klasse geschrieben, die "Worker" wie folgt generiert.
class Ninsoku
def initialize(task, worker_count: 10)
@task = task
@pipe = create_pipe
@workers = create_workers(worker_count)
end
def send(arg)
@pipe.send arg
end
def run
yield Ractor.select(*@workers)
end
def create_pipe
Ractor.new do
loop do
Ractor.yield Ractor.recv
end
end
end
def create_workers(worker_count)
(1..worker_count).map do
Ractor.new @pipe, @task do |pipe, task|
loop do
arg = pipe.take
task.send arg
Ractor.yield task.take
end
end
end
end
end
Ninsoku.new
erzeugt Pipe
und Worker
. Außerdem übergibt "task" den von "Ractor" zu verarbeitenden Inhalt und führt ihn von "worker" aus.
Es sieht so aus, als würde man es tatsächlich verwenden.
task = Ractor.new do
func = lambda{|n| n.downcase }
loop do
Ractor.yield func.call(Ractor.recv)
end
end
ninsoku = Ninsoku.new(task)
('A'..'Z').each{|i|
ninsoku.send i
}
('A'..'Z').map{
ninsoku.run{|r, n|
puts n
}
}
# => a ~Bis zu z werden parallel ausgegeben
~~ Ich denke, ich werde diese Klasse später bei "gem" ausprobieren. ~~
Ich habe versucht, es zu einem Edelstein zu machen (ich habe nicht auf Rubin-Edelsteine gedrängt ...)
Lassen Sie uns beispielsweise die AED-Standortinformationen von Hamada City, Präfektur Shimane, wo ich wohne, mit Ractor verarbeiten. Für die AED-Standortinformationen der Präfektur Shimane haben wir die offenen Daten verwendet, die von der Präfektur Shimane veröffentlicht wurden. Ich möchte diese Gelegenheit nutzen, um Ihnen zu danken.
Open Data Catalog Site der Präfektur Shimane
require "rorker"
require "csv"
task = Ractor.new do
func = lambda{|row|
row.map{|value|
if value =~ /Hamada Stadt/
row
end
}.compact
}
loop do
Ractor.yield func.call(Ractor.recv)
end
end
rorker = Rorker.new(task)
csv = CSV.read "a.csv"
csv.each do |row|
rorker.send row
end
n = 0
while n < csv.count
rorker.run{|worker, result|
if !result.empty?
puts result
end
}
n += 1
end
Sie können den so gelesenen CSV auch zeilenweise an den Worker übergeben und die für die Parallelverarbeitung erforderlichen Daten abrufen.
Da Ractor
den Prozess in Blöcken durchläuft, können Sie auch Numbered Parameter
verwenden, um das Argument zu erhalten.
r = Ractor.new :hoge do
puts _1
end
r.take
# => hoge
Übrigens funktioniert es mit mehreren Argumenten.
r = Ractor.new :hoge, :hoge do
puts _1
puts _2
end
r.take
# => hoge
# => fuga
Wenn Sie mehr als eine übergeben, werden sie anscheinend in der Reihenfolge, in der sie übergeben wurden, von "_1" an "_9" übergeben.
Übrigens, wenn Sie Hash passieren, wird es so aussehen.
r = Ractor.new ({hoge: 1, fuga: 2}) do
_1.map do |key, value|
p ":#{key} => #{value}"
end
end
r.take
# => ":hoge => 1"
# => ":fuga => 2"
Hash mit =>
ergab ähnliche Ergebnisse
r = Ractor.new({:hoge => 1, :fuga => 2}) do
_1.map do |key, value|
p ":#{key} => #{value}"
end
end
r.take
# => ":hoge => 1"
# => ":fuga => 2"
Im Fall von Array ist das Verhalten jedoch etwas anders.
r = Ractor.new [1, 2, 3] do
puts _1
puts _1.class
puts _2
puts _2.class
puts _3
puts _3.class
end
r.take
#=> 1
#=> Integer
#=> 2
#=> Integer
#=> 3
#=> Integer
Anscheinend wird es in der Reihenfolge vom Anfang des Arrays an übergeben, wie wenn mehrere Argumente wie gewohnt übergeben werden. Vielleicht wird es wie folgt interpretiert.
_1, _2, _3 = [1, 2, 3]
Übrigens, wenn Sie ein Array übergeben, das größer ist als die Nummer, die vom nummerierten Parameter empfangen werden kann
r = Ractor.new [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] do
puts _1
puts _2
puts _3
puts _4
puts _5
puts _6
puts _7
puts _8
puts _9
end
r.take
#=> 1
#=> 2
#=> 3
#=> 4
#=> 5
#=> 6
#=> 7
#=> 8
#=> 9
Es scheint, dass Sie es so bis zu dem Bereich bekommen können, in dem Numbered Parameter
empfangen werden kann
Bei Verwendung von "Numbered Parameter" in "Ractor", wenn "Hash" als Argument übergeben wird
r = Ractor.new ({hoge: 1, fuga: 2}) do |hash|
hash.map do
p ":#{_1} => #{_2}"
end
end
r.take
":hoge => 1"
":fuga => 2"
Oder es scheint verwendet zu werden, wenn Sie es beim Übergeben einiger Argumente weglassen möchten
r = Ractor.new :hoge, :fuga do
p _1
p _2
end
r.take
# => :hoge
# => :fuga
Wir hoffen, dass Sie diesen Artikel lesen und sich für Ractor
interessieren.
Ich werde weiterhin den Code hinzufügen, den ich mit Ractor
versucht habe
Recommended Posts