LoginSignup
4

More than 5 years have passed since last update.

[実験]Rubyでマルチスレッド・マルチプロセスの挙動の違いを調べてみた

Last updated at Posted at 2017-08-07

はじめに

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ではどうなのでしょうか?

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
4