はじめに
Rubyの並列処理について、いくつか基本的なケースの挙動を確認してみます。具体的には、マルチプロセス・マルチスレッド・GIL(※1)・排他ロックあたりの挙動を見ていきます。
今回使用した環境は以下のとおりです。
Rubyのバージョン: ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux]
parallelのバージョン: parallel (1.9.0)
並列処理のさまざまなケース
並列処理の条件を変えてcounter変数をインクリメントし、counter変数のふるまいの違いを調べます。
マルチプロセスの場合
counter = 0
Parallel.each(1..100, in_processes: 2) do # 2プロセスでインクリメントする
counter += 1
print "%d, " % counter
end
# 結果:
# 1, 1, 2, 3, 4, 5, ..., 60, 36, 61, 62, 37, 63,
プロセスごとに別のcounterが作られるため、最大値が100になりません。
ところで、最大値が100にならないのは、counterのインクリメントに対して排他制御がかかっていないためとも考えられます(※2)。それでは、次のケースを試してみます。
マルチプロセスで排他ロックをかけた場合
counter = 0
Parallel.each(1..100, in_processes: 2) do # 2プロセスでインクリメントする
open(File.join(Dir.tmpdir, 'counter.lock')) do |f|
begin
f.flock(File::LOCK_EX)
counter += 1
print "%d, " % counter
ensure
f.flock(File::LOCK_UN)
end
end
end
# 結果:
# 1, 2, 3, 4, 5, ..., 17, 18, 19, 20, 21, 79,
counterのインクリメントに対して排他制御をかけても、最大値が100になりません(※2)。このことから、プロセスごとに別のcounterが作られていることがわかります。
マルチスレッドでブロッキングIOがある場合
counter = 0
Parallel.each(1..100, in_threads: 10) do # 10スレッドでインクリメントする
counter += 1
print "%d, " % counter
end
# 結果:
# 1, 2, 3, 4, 5, ..., 100, 87, 94, 95, 89, 97, 91, 99, 92,
異なるスレッドでも同じcounterを引き継ぐので、最大値が100になります。数値が1から順番にならないことがあるのは、printのブロッキングIO処理でGILが外れるためです。
マルチスレッドでブロッキングIOがある場合(排他ロックつき)
counter = 0
Parallel.each(1..100, in_threads: 10) do # 10スレッドでインクリメントする
open(File.join(Dir.tmpdir, 'counter.lock')) do |f|
begin
f.flock(File::LOCK_EX)
counter += 1
print "%d, " % counter
ensure
f.flock(File::LOCK_UN)
end
end
end
# 結果:
# 1, 2, 3, 4, 5, ..., 95, 96, 97, 98, 99, 100
# (つねに1から順番どおりに並ぶ)
counterのインクリメントとその後のprintに対して排他制御をかけると、数値がつねに1から順番に並ぶようになりました。printでGILが外れても他のスレッドが割り込まなくなることが確認できます。
マルチスレッドでブロッキングIOがない場合
counter = 0
counters = []
Parallel.each(1..100, in_threads: 10) do # 10スレッドでインクリメントする
counter += 1
counters << counter
end
puts counters.join(', ')
# 結果:
# 1, 2, 3, 4, 5, ..., 95, 96, 97, 98, 99, 100
# (つねに1から順番どおりに並ぶ)
マルチスレッドのブロックからブロッキングIO処理を外してみると、何回試しても数値が1から順番になります。GILのはたらきにより、並列化がまったく行われていないことがわかります。
補足
※1. GIL(Global Interpreter Lock)とは、スレッドセーフティを保つため、スレッド全体に対してかかる大きなロックです。RubyがCによる実装のレベルでスレッドセーフでないこと、細かい粒度でロックを設定するとシングルスレッドの場合にパフォーマンスが落ちることが、GILが実装されている主な理由です。
ブロッキングIOによる待ちが発生すると、一時的にGILは外れるようになっています。
※2. Javaなどでは、値のインクリメントは
・値の読み込み
・読み込んだ値に1を足して書き込み
の2つのプロセスが非アトミックに行われます。もしインクリメントの処理がアトミックならば、もともと排他ロックは必要ありません。Rubyではどうなのでしょうか?