0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ruby 4.0 の Ractor 使ってみた (4) Ractor に馴染む

0
Last updated at Posted at 2026-04-21

Ractor

いよいよ Ractor を触ってみる。

Ractor の生成

Ractor は Ractor.new で作れるが,そのとき必ずブロックを与えないといけないようだ。

以下を実行してみよう。

Ractor.new do
  puts "Hello!"
end
# => "Hello!"

sleep 1 # ← あとで説明する

ええと,

warning: Ractor API is experimental and may change in future versions of Ruby.

という警告が出るが,気にしない。
Ruby 4.0 では Ractor はまだ「実験的」ということ。

どうも Ractor は,生成しただけでいきなりブロックの実行をおっぱじめるようだ。しかもブロックの実行が終わったらとっとと終了してしまう(上記スクリプトも終了する)。

Ractor に次々と仕事を与えて動かし続けるには,ブロックの中にループを書かないといけないようだ。

なぜ sleep

で,最後に書いた sleep 1(1 秒待つ)が気になるよね?
私もよく分からないが,Ractor(のブロック)は非同期に動くので,ブロックの実行が終わる前にメインの流れが終わる(スクリプトが終了する)ことがあるようだ。
つまり,sleep 1 が無いと「Hello!」が表示されない(表示される前にスクリプトが終了する)場合があるということ。

※「メインの流れ」と書いたが,これは暗黙の Ractor である「メイン Ractor」の処理のこと(シリーズ (3) で言及)。

実際,sleep 1 無しだと,「Hello!」が表示されたりされなかったりした。
sleep 1 を入れた場合,確実に表示されるようになった。

しかし,「1 秒待つ」よりマトモな方法は無いのか?
ある。

生成した Ractor の join メソッドを呼び出すと,その Ractor の(ブロックの)終了を待つらしい。
つまり,

Ractor.new do
  puts "Hello!"
end.join
# => "Hello!"

とでも書けば,「Hello!」は確実に表示される。

もし,

Ractor.new do
  sleep 5
  puts "Hello!"
  sleep 5
end.join

puts "Goodbye!"

と書けば,スクリプトを実行させて 5 秒くらいしてから「Hello!」が表示され,それからさらに 5 秒くらいしてから「Goodbye!」が表示されることになる。

join という名前は Array#join とは何の関係も無い。スレッドを知っている人には「ああ,あの join ね」と分かるらしいが,そうでない人(私を含む)には,いったい何を繋ぐのか?と思うだろう。

外界とのやりとり

ところで,Ractor のブロックには一つ大きな特徴がある。
以下を見てみよう。

name = "Ruby"

Ractor.new do
  puts "Hello, #{ name }!"
end
# => ArgumentError

ArgumentError で死んだ。
エラーメッセージは

can not isolate a Proc because it accesses outer variables (name). (ArgumentError)

だ。
「外部の変数にアクセスしてるから Proc が isolate(分離)できない」とかなんとか言っているようだ。

Ruby のブロックは一般に,その外で有効なローカル変数にアクセスできるはず。たとえば

name = "Ruby"

3.times do
  puts "Hello, #{ name }!"
end
# => Hello, Ruby!
#    Hello, Ruby!
#    Hello, Ruby!

のように。

しかし,Ractor に与えたブロックは,そうすることを許されていないようだ。
件のエラーメッセージは,「Ractor のブロックは外の世界と分離されなきゃいけないのに,外のローカル変数を参照しちゃってるから分離できないじゃん」と言ってるのかな,と思う。

外部の定数を参照するのは問題ないようだ。

定数を除けば,Ractor(のブロック)が外界とやりとりするためには,ポートを使うしかないのだろう。

Ractor にオブジェクトを渡す

Ractor にオブジェクトを渡してみよう。

文字列を受け取って,「Hello, なんちゃら!」と表示する Ractor を定義してみるのだ。

Ractor(のブロック)が外界からオブジェクトを受け取るには Ractor.receive というメソッドを使うことができる。
この場合,Ractor にオブジェクトを渡すには,Ractor#send というインスタンスメソッドが使える。

ractor = Ractor.new do
  puts "Hello, #{ Ractor.receive }!"
end

ractor.send "Ruby" # => Hello, Ruby!
ractor.join

おおー,いいね。
動作はたぶんこういうことだと思う:

  • Ractor が生成されるとすぐにブロックの評価が始まる
  • ブロック内でまず,puts の引数である "Hello, #{ Ractor.receive }!" を評価しようとする
  • ここには式展開で Ractor.receive があり,これを評価しようとして,(外部から与えられるはずのオブジェクトの)待ちが生じる
  • 「待ち」といっても,このブロックはメイン Ractor とは並列に動いている
  • よって,「生成した Ractor をローカル変数 ractor に代入」とか「ractor.send "Ruby"」……という処理は,それはそれでどんどん進む
  • ractor.send "Ruby" が実行されることで,「待ち」になってた Ractor のブロックの評価も進む
  • ractor.join があるので,ractor のブロックの実行が終わる前にメイン Ractor の実行が終わってしまうことはない

なお,send というメソッドは,Ractor::Port にも Ractor にもあるので,ごっちゃにしないように。

ちょっと待てぇ!

ちょっと待って。
少し前に,Ractor が外界とやり取りするのにポートが必須だとか書かなかったか?

はい,書きました。
実は全ての Ractor が「暗黙のポート」を一つ持っている。これを,その Ractor のデフォルトポートと呼ぶらしい。
Ractor#send はその暗黙のポートにオブジェクトを渡すもの。
Ractor.receive自分の暗黙のポートからオブジェクトを受け取るものなのだ。

ポートからの受け取りについての制約

ポートからのオブジェクト受け取りには,強い制約がある。
それは,「自分のポートからしか受け取れない」,言い換えれば「そのポートが属する Ractor でしか受け取れない」ということ。

だからたとえば,ある Ractor のデフォルトポートからオブジェクトを受け取れるのはその Ractor だけ,ということになる。

また,外部で生成したポートを Ractor に与えたとして,その Ractor はそのポートにオブジェクトを渡すことはできても,そこから受け取ることはできない。

一方,ポートにオブジェクトを渡すのは誰でもできるようだ。

Ractor からオブジェクトを受け取る

次に,あらわに生成した Ractor から,メイン Ractor がオブジェクトを受け取ることを考えてみよう。

まずメイン Ractor でポートを作っておき,そのポートを,あらわに作った Ractor に渡す。
その Ractor 内で,与えられたポートにオブジェクトを渡す。
そうすれば,メイン Ractor 側でその値を受け取ることができそうだ。

問題は,そのポートをどうやって Ractor に渡すか。

Ractor 生成時に,Ractor.new の引数としてオブジェクトを渡すと,それをブロックパラメーターとして受け取ることができるので,それを使おう。

port = Ractor::Port.new

ractor = Ractor.new port do |po|
  po.send "Hello, #{ Ractor.receive }!"
end

ractor.send "Ruby"

puts port.receive
# => Hello, Ruby!

Ractor.new に与えた引数がそのままブロックパラメーターになるのだ。複数のオブジェクトを受け渡すこともできる。
上記のサンプルコードでは,理解しやすくするため,外部のローカル変数を port とし,ブロックパラメーターを po と名前を変えているが,

ractor = Ractor.new port do |port|
  # 云々
end

と同じ名前にしても構わない。
というか毎回別の名をつけるのも面倒なので,実務のコードでは同じ名前を使うことにすればいいと思う。

ブロックの評価が終わった Ractor

ブロックの評価が終わっても Ractor オブジェクト自体が消滅したりはしないようだ。

以下の実験をやってみよう。

ractor = Ractor.new do
  puts "Hello, #{ Ractor.receive }!"
end

ractor.send "Ruby" # => Hello, Ruby!

sleep 1

ractor.send "Python"
# => The port was already closed (Ractor::ClosedError)

sleep 1

Ractor::ClosedError で死んだ。
エラーメッセージを読むと,「ポートは既に閉じている」と言っているようだ。

ブロックの評価が終わった Ractor オブジェクトは,デフォルトポートが閉じているようだ。


次回は,Ractor をどのように使ってこれを並列化するか,その方針について述べる。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?