[RUBY] Ractor super Einführung

Einführung

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.

Umgebung

Was ist Ractor?

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.newDas 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 Code

Raktorgenerierung

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.

Übergeben Sie Argumente an Ractor

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

Rückgabewert in Ractor

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

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."

Parallele Ausführung in Ractor

Einfaches Beispiel

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.

Ein kleines Beispiel

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.

Schreiben wir eine Klasse, die Arbeiter usw. erstellt und verarbeitet.

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 ...)

S-H-GAMELINKS/rorker

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.

Verwenden Sie nummerierte Parameter mit Ractor

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

abschließend

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

Referenz

Recommended Posts

Ractor super Einführung
Groovy super einfache Einführung
[Für Super-Anfänger] DBUnit Super-Einführung
[Für Super-Anfänger] Ameise Super-Einführung
[Für Super-Anfänger] Maven Super-Einführung
[Für Super-Anfänger] Mirage SQL Super-Einführung
[Super Einführung] Über Symbole in Ruby
Lombok ① Einführung
Einführung (Selbsteinführung)
[Java] Einführung
Einführung (Bearbeitung)