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.new
L'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 worker
s 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 worker
s 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