📌

【Ruby】13行で動く!平行処理入門

2024/01/29に公開

明示的な平行処理実行の簡単な例をRubyを使って解説してみます。
最初に今回の実装コードを提示して、その後に解説パートが続く構成になっています。

平行処理って何

平行処理とは、実行されたメインスレッド内で複数のサブスレッドを立てて、サブスレッド数分のタスクを完了させることです。(マルチスレッドともいいますね)
各用語に対する具体的な解説はここでは省いています。

平行処理のわかりやすい説明記事
https://zenn.dev/hsaki/books/golang-concurrency/viewer/term

実装コード

parallel_test.rb
trread_01 = Thread.new do
  5.times do |i|
    p "thread_01: #{i}"
  end
end

trread_02 = Thread.new do
  5.times do |i|
    p "thread_02: #{i}"
  end
end

[ trread_01, trread_02 ].each(&:join)
% ruby parallel_test.rb

"thread01: 0"
"thread01: 1"
"thread01: 2"
"thread01: 3"
"thread01: 4"
"thread02: 0"
"thread02: 1"
"thread02: 2"
"thread02: 3"
"thread02: 4"

解説

Thread class

rubyではThreadを使用することで、新しいサブスレッドを作成することができます。
tread_result = Thread.new do ~ endの記述でサブスレッド単位の処理を記述し、返却値を変数に格納しています。

実装コードの処理詳細は、5.timesで5回のイテレーションを行い、引数の値を出力しているだけです。
また、trread_01trread_02の計二つのサブスレッドを作成しており、後続のメインスレッド内での処理はどちらの処置結果も扱える状態になります。

サブスレッドの実行タイミング

実装コードで二つのサブスレッドを作成していますが、上から順にスレッド処理が行われるわけではありません。
二つのサブスレッドはメインスレッド内で、同時進行で処理されていきます。
コードを何度か実行してみて処理結果を比べていきます。

% ruby parallel_test.rb
"thread02: 0"
"thread01: 0"
"thread01: 1"
"thread02: 1"
"thread01: 2"
"thread02: 2"
"thread01: 3"
"thread02: 3"
"thread01: 4"
"thread02: 5"

% ruby parallel_test.rb
"thread01: 0"
"thread02: 0"
"thread01: 1"
"thread02: 1"
"thread01: 2"
"thread02: 2"
"thread01: 3"
"thread02: 3"
"thread01: 4"
"thread02: 4"

% ruby parallel_test.rb
"thread01: 0"
"thread01: 1"
"thread01: 2"
"thread01: 3"
"thread02: 0"
"thread02: 1"
"thread02: 2"
"thread02: 3"
"thread02: 4"
"thread01: 4"

こんな感じで出力順が毎回違います。
メインスレッドで実行されている[ trread_01, trread_02 ].each(&:join)も、サブスレッドの完了を待たずに来た順で処理しているので、早く処理が終わったサブスレッドの返却値からシェルに出力されていきます。

joinメソッドとメインスレッドの制御

最終行の[ trread_01, trread_02 ].each(&:join)についてです。
配列呼び出しをレシーバとする.each(&:join)部分を消して実行してみます。

parallel_test.rb
trread_01 = Thread.new do
  5.times do |i|
    p "thread_01: #{i}"
  end
end

trread_02 = Thread.new do
  5.times do |i|
    p "thread_02: #{i}"
  end
end

[ trread_01, trread_02 ]
 % ruby parallel_test.rb
 

出力の宣言は正しくされていますが、何も出ずに終わってしまいました。なぜでしょうか?
答えはメインスレッドの性質にあります。
先ほどの記述では、サブスレッド処理完了前にメインスレッドの実行が終わってしまったことが、何の出力も無しに終わった原因です。
サブスレッドがそれぞれ配列に文字列を格納していっていますが、それとは別でメインスレッドも自身の処理をサブスレッドと同時進行で進めています。
なので [ trread_01, trread_02 ]が空の時点でメインスレッドが処理してしまい、出力がないのが先ほどの不具合の理由です。

この現象を避けるために平行処理では「サブスレッド処理が終わるまでメインスレッドを止める」為の実装が必要になります。
今回それを担っているのが.each(&:join)の記述です。
symbol_to_blockで配列内の全ての要素のjoinメソッドを実行しています。
スレッド内で実行されるjoinメソッドは、レシーバが生成されるスレッドが完了するまでjoinが実行されるスレッド(今回はメインスレッド)を止める機能を持つため、メインスレッドがサブスレッドの結果を待ってから実行することができています。

その他にメインスレッドを止める方法としては、rubyのslseepを使用してスレッドの完了を時間ごと遅らせる方法があります。

parallel_test.rb
trread_01 = Thread.new do
  5.times do |i|
    p "thread01: #{i}"
  end
end

trread_02 = Thread.new do
  5.times do |i|
    p "thread02: #{i}"
  end
end

[ trread_01, trread_02 ]
sleep(5)
% ruby parallel_test.rb
"thread01: 0"
"thread01: 1"
"thread01: 2"
"thread01: 3"
"thread01: 4"
"thread02: 0"
"thread02: 1"
"thread02: 2"
"thread02: 3"
"thread02: 4"

# 5秒待つ

今回は以上になります。
ここまで読んでいただき、ありがとうございました😄

Discussion