[RUBY] Super introduction de Ractor

introduction

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.

environnement

Qu'est-ce que 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.

Code Ractor

Génération de racteurs

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

Passer des arguments à Ractor

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

Valeur de retour dans Ractor

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

Exceptions dans Ractor

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

Exécution parallèle dans Ractor

Exemple simple

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.

Un petit exemple

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.

Écrivons une classe qui crée et traite des travailleurs, etc.

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

S-H-GAMELINKS/rorker

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.

Utiliser le paramètre numéroté avec Ractor

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

en conclusion

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

référence

Recommended Posts

Super introduction de Ractor
Introduction super facile Groovy
[Pour les super débutants] Super introduction à DBUnit
[Pour les super débutants] Ant super introduction
[Pour les super débutants] Super introduction à Maven
[Pour les super débutants] Super introduction à Mirage SQL
[Super Introduction] À propos des symboles dans Ruby
Lombok ① Introduction
Introduction (auto-introduction)
[Java] Introduction
Introduction (édition)