Cet article s'adresse à ceux qui sont intéressés par la nouvelle fonctionnalité Ractor pour le traitement parallèle / parallèle introduite dans Ruby3.
Tout en expliquant l'exemple de code à l'aide d'un simple Ractor, je l'ai écrit pour approfondir ma compréhension (même si cela semble être un article pour moi dans le futur ...).
C'est aussi un article qui résume les résultats de la recherche de ce qui peut être fait avec «Ractor» lui-même.
Par conséquent, la seconde moitié explique le comportement basé sur le code lors de la lecture de différentes manières avec Ractor.
Je suis sûr que certaines personnes ne connaissent pas Ractor en premier lieu, alors je vais vous le présenter brièvement.
Ractor est une nouvelle fonction de traitement parallèle / parallèle introduite dans Ruby3. La fonctionnalité elle-même est proposée depuis plusieurs années, à l'époque sous le nom de «Guilde».
Cependant, il y avait une voix de l'industrie du jeu disant: "J'utilise le nom Guild, donc j'aimerais que vous utilisiez un nom différent", et il a changé pour l'actuel Ractor.
Il semble qu'il soit basé sur le modèle ʻActor, donc il a été renommé Ractor (Ruby's Actor)`.
«Ractor» est une unité d'exécution parallèle, et chacun est exécuté en parallèle. Par exemple, dans le code ci-dessous, «met: hello» et «met: hello» sont exécutés en parallèle.
Ractor.new do
  5.times do
    puts :hello
  end
end
5.times do
    puts :world
end
L'exécution de ce code donne le résultat suivant:
world
helloworld
hello
world
helloworld
helloworld
hello
De cette manière, chaque processus peut être exécuté en parallèle.
Le Ractor peut également envoyer et recevoir des objets à un autre Ractor et les exécuter de manière synchronisée. Il existe deux méthodes de synchronisation, «type push» et «type pull».
Par exemple, dans le cas du "type push", le code sera le suivant.
r1 = Ractor.new do
    :hoge
end
r2 = Ractor.new do
    puts :fuga, Ractor.recv
end
r2.send(r1.take)
r2.take
# => :fuga, :hoge
Avec Ractor, vous pouvez utiliser la méthode send pour envoyer un objet à un autre Ractor. Avec le code ci-dessus
r2.send(r1.take)
Il envoie à «r2» dans la partie de.
Les objets soumis peuvent être reçus dans Ractor avec Ractor.recv
r2 = Ractor.new do
    puts :fuga, Ractor.recv # 
end
Vous pouvez prendre l'objet envoyé par r2.send (r1.take) et le passer à la méthode put.
Il utilise également la méthode «take» pour recevoir le résultat de l'exécution du «Ractor».
Donc r1.take reçoit: hoge.
En d'autres termes, «r2.send (r1.take)» reçoit le résultat de l'exécution de «r1» et l'envoie à «r2». Et «met: fuga, Ractor.recv» dans «r2» devient «met: fuga ,: hoge», ce qui signifie que «fuga» et «hoge» sont respectivement sortis.
C'est le flux d'échange d'objets avec le "type push".
D'autre part, pull type a le code suivant.
r1 = Ractor.new 42 do |arg|
    Ractor.yield arg
end
r2 = Ractor.new r1 do |r1|
    r1.take
end
puts r2.take
Ractor.newL'argument transmis|arg|Peut être reçu sous forme de variable utilisable dans le bloc.
Par exemple, le code suivant attend que «r1» exécute la méthode «take».
r1 = Ractor.new 42 do |arg|
    Ractor.yield arg
end
Vous pouvez également passer un autre Ractor à Ractor.new, afin que vous puissiez écrire:
r2 = Ractor.new r1 do |r1|
    r1.take
end
Vous pouvez maintenant recevoir le 42 que r1 a reçu comme argument dans r2.
Enfin, "met r2.take" reçoit et sort "42".
Le type pull est comme ça.
Expliquez grossièrement
--push type: Ractor # send + Ractor.recv
--pull type: Ractor.yield + Ractor # take
C'est comme ça.
Pour une explication plus détaillée de «Ractor», veuillez vous référer au lien ci-dessous.
«Ractor» est «Ractor.new» et écrit le processus que vous souhaitez exécuter dans le bloc.
Ractor.new do
  #Ce bloc fonctionne en parallèle
end
Le traitement dans ce bloc est exécuté en parallèle.
En d'autres termes, dans le cas du code suivant
Ractor.new do
    10.times do
        puts :hoge
    end
end
10.times do
    puts :fuga
end
: hoge et: fuga sont affichés en parallèle.
De plus, puisque le processus que vous voulez exécuter est passé sous forme de bloc à Ractor.new, vous pouvez également écrire comme suit.
Ractor.new{
    10.times{
        puts :hoge
    }
}
Vous pouvez également le nommer en utilisant l'argument mot-clé nom, et vous pouvez également recevoir le nom avec Ractor # name.
r = Ractor.new name: 'r1' do
    puts :hoge
end
p r.name
# => "r1"
Cela vous permettra également de voir quel Ractor exécute le processus.
Vous pouvez passer un objet à l'intérieur d'un bloc en passant un argument à Ractor.new.
r = Ractor.new :hoge do |a|
    p a
end
r.take
# => :hoge
Vous pouvez passer des objets via des arguments de cette manière.
Vous pouvez également passer plusieurs arguments
r = Ractor.new :hoge, :fuga do |a, b|
    p a
    p b
end
r.take
# => fuga
# => hoge
Vous pouvez également passer ʻArray` comme ceci.
r = Ractor.new [:hoge, :fuga] do |a|
    p a.inspect
end
r.take
# => "[:hoge, :fuga]"
Au fait,|a|À|a, b|Si vous changez pour
r = Ractor.new [:hoge, :fuga] do |a, b|
    p a
    p b
end
r.take
# => :hoge
# => :fuga
Le résultat de sortie sera. Cela semble être interprété comme le même comportement que ʻa, b = [: hoge ,: fuga] `.
Aussi, dans le cas de "Hash"
r = Ractor.new({:hoge => 42, :fuga => 21}) do |a|
    p a
    p a[:hoge]
end
r.take
# => {:hoge=>42, :fuga=>21}
# => 42
Est sortie.
Au fait, si vous ne le placez pas avec () après Ractor.new, ce sera SyntaxError, alors soyez prudent.
r = Ractor.new({:hoge => 42, :fuga => 21}) do |a|
    p a
    p a[:hoge]
end
r.take
# => SyntaxError
Ractor peut recevoir la valeur de retour dans le bloc exécuté avec la méthode take.
r = Ractor.new do
    :hoge
end
p r.take
# => :hoge
Au fait, si vous faites return dans le bloc, cela semble être LocalJumpError.
r = Ractor.new do
    return :fuga
    :hoge
end
p r.take
# => LocalJumpError
Les exceptions dans Ractor peuvent être reçues comme suit:
r = Ractor.new do
    raise 'error'
end
begin
    r.take
rescue Ractor::RemoteError => e
    p e.message
end
En passant, vous pouvez également écrire la gestion des exceptions dans Ractor.
r = Ractor.new name: 'r1' do
    begin
        raise 'error'
    rescue => e
        p e.message
    end
end
r.take
De plus, selon la documentation, il semble que vous puissiez intercepter l'exception dans la zone qui reçoit la valeur renvoyée depuis le bloc Ractor. En d'autres termes, vous pouvez également écrire le code suivant.
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."
Vous pouvez exécuter en parallèle avec Ractor comme ceci.
Ractor.new do
    3.times do
        puts 42
    end
end
3.times do
    puts 21
end
Lorsqu'elles sont exécutées, les sorties «42» et «21» seront affichées séparément.
Vous pouvez générer plusieurs workers avec Ractor, les transmettre via pipe, transmettre des valeurs et résumer les résultats comme indiqué ci-dessous.
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 ~Affiche le résultat indiquant si les nombres jusqu'à 999 sont des nombres premiers
Ce code crée 10 «workers» et passe un objet à chaque «worker» via «pipe».
Il retourne également l'objet reçu avec Ractor.yield [n, n.prime?].
Vous pouvez créer plusieurs workers comme celui-ci, les traiter via pipe et recevoir les résultats.
Avec le code précédent, le traitement dans worker était susceptible de devenir volumineux plus tard, j'ai donc écrit une classe qui générera bien worker comme suit.
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» génère «pipe» et «worker». De plus, «task» transmet le contenu à traiter par «Ractor» et l'exécute par «worker».
Cela ressemble à un étui à utiliser.
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 ~Jusqu'à z sont émis en parallèle
~~ Je pense que j'essaierai cette classe plus tard sur gem. ~~
J'ai essayé d'en faire un `` bijou '' (je n'ai pas poussé aux gemmes de rubis ...)
Par exemple, traitons les informations de localisation du DAE de la ville de Hamada, préfecture de Shimane, où je vis, à l'aide de Ractor. Pour les informations de localisation AED de la préfecture de Shimane, nous avons utilisé les données ouvertes publiées par la préfecture de Shimane. Je voudrais profiter de cette occasion pour vous remercier.
Site du catalogue de données ouvertes de la préfecture de Shimane
require "rorker"
require "csv"
task = Ractor.new do
  func = lambda{|row| 
    row.map{|value|
      if value =~ /Ville de Hamada/
        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
Vous pouvez également transmettre la lecture CSV comme ceci au travailleur ligne par ligne et récupérer les données nécessaires pour le traitement parallèle.
Puisque Ractor transmet le processus par blocs, vous pouvez également utiliser Paramètre numéroté pour recevoir l'argument.
r = Ractor.new :hoge do
    puts _1
end
r.take
# => hoge
Au fait, cela fonctionne avec plusieurs arguments.
r = Ractor.new :hoge, :hoge do
    puts _1
    puts _2
end
r.take
# => hoge
# => fuga
Si vous passez plus d'un, il semble qu'ils soient passés de _1 à _9 dans l'ordre dans lequel ils ont été passés.
Au fait, si vous passez Hash, cela ressemblera à ceci.
r = Ractor.new ({hoge: 1, fuga: 2}) do
    _1.map do |key, value|
        p ":#{key} => #{value}"
    end
end
r.take
# => ":hoge => 1"
# => ":fuga => 2"
Le hachage avec => a donné des résultats similaires
r = Ractor.new({:hoge => 1, :fuga => 2}) do
    _1.map do |key, value|
        p ":#{key} => #{value}"
    end
end
r.take
# => ":hoge => 1"
# => ":fuga => 2"
Cependant, dans le cas de Array, le comportement est légèrement différent.
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
Apparemment, il est passé dans l'ordre depuis le début du tableau, comme lorsque plusieurs arguments sont passés comme d'habitude. Peut-être est-il interprété comme suit.
_1, _2, _3 = [1, 2, 3]
Au fait, si vous passez un ʻArray qui est plus grand que le nombre qui peut être reçu par le paramètre numéroté`
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
Il semble que vous puissiez l'obtenir comme ça jusqu'à la plage où le paramètre numéroté peut être reçu
Lorsque vous utilisez Numbered Parameter dans Ractor, lorsque Hash est passé comme argument
r = Ractor.new ({hoge: 1, fuga: 2}) do |hash|
    hash.map do
        p ":#{_1} => #{_2}"
    end
end
r.take
":hoge => 1"
":fuga => 2"
Ou il semble être utilisé lorsque vous souhaitez l'omettre lors du passage de certains arguments
r = Ractor.new :hoge, :fuga do
    p _1
    p _2
end
r.take
# => :hoge
# => :fuga
Nous espérons que vous lirez cet article et que vous vous intéresserez à «Ractor».
Je vais continuer à ajouter le code que j'ai essayé en utilisant Ractor
Recommended Posts