備忘録的な何か

技術ブログ的な何かです

CRuby(MRI)はスレッドセーフなのか

少々炎上しそうな表題ですが、色々と調べたので覚書。

まず、前提条件として、CRubyはGIL(Global Interpreter Lock)という仕組みがあります。
細かい説明は
グローバルインタプリタロック - Wikipedia
を参照いただくとして、Rubyインタプリタは必ずGILを取得する必要があります。

となると、Thread意味無いじゃんと思ってしまうのですが、RubyにおけるGIL=GVL(Giant VM Lock)は、class Threadにあるように、IO関連処理のみ並列実行してくれます。

ネイティブスレッドを用いて実装されていますが、 現在の実装では Ruby VM は Giant VM lock (GVL) を有しており、同時に実行される ネイティブスレッドは常にひとつです。 ただし、IO 関連のブロックする可能性があるシステムコールを行う場合には GVL を解放します。その場合にはスレッドは同時に実行され得ます。 また拡張ライブラリから GVL を操作できるので、複数のスレッドを 同時に実行するような拡張ライブラリは作成可能です。


また、CRuby以外の実装について言及しますと、JRuby,RubiniusともにGILは存在せず、並列実行が可能です。

それでは、本題に戻りまして、CRubyがスレッドセーフなのかについて考えていきたいと思います。
まず、
Ruby core classes aren't thread-safe
を見ますと、Hashへのデクリメント操作の結果が、

MRI 1.9.3 : 0
JRuby 1.7.2 : 486
RBX 2.0.0rc1 : 2

となってきており、CRubyのHashはスレッドセーフに実行されていることがわかります。
(内容を端折っていますが、Hashの1つの要素に4000という値を入れて、マルチスレッドで4000回デクリメントした結果になります。)

この結果から、引用先の記事には次のような記述があります。

This is what the tweet alluded to. Our experience with MRI may lead us to think that the core classes are thread-safe, but this is an 'accident' of MRI that really shows itself as an accident when you run the same code on other runtimes

要するに、たまたまCRubyはスレッドセーフ的に動いてますよということですね。

では、次に
Nobody understands the GIL - Part 2: Implementation

を見てみますと、Charles Nutter氏(JRuby開発者)がコメントで次のように述べています。

Put simply, only standalone C functions are "atomic" in MRI.

なるほど、C言語実装の関数単独ではatomicということですね。
C functionsがatomicということは、CRubyはスレッドセーフなのか....そうはいかないらしいです。

Does the GIL Make Your Ruby Code Thread-Safe?
を見てみます。

引用先に次のようなコードが有ります。

class Sheep
  def initialize
    @shorn = false
  end

  def shorn?
    @shorn
  end

  def shear!
    puts "shearing..."
    @shorn = true
  end
end

sheep = Sheep.new

5.times.map do
  Thread.new do
    unless sheep.shorn?
      sheep.shear!
    end
  end
end.each(&:join)

元記事を読んでいただくほうが良いと思いますが、私なりにコードの説明をしますと、
Sheepクラスは、shornメソッドとshear!メソッドがあります。
毛を刈り込まれていない羊(!shorn?)が存在すれば、毛を刈り込みます(shear!)。
このコードの実装者の意図は、一匹の羊は、一度しか刈り込み(share!)を実行できないということになります。

しかし、このコードを実行すると、1つの羊に対してshear!が複数回呼ばれてしまうことがあります。
その理由は、shorn?とshare!が同期実行されないからです。

shorn?とshare!の同期実行を保証するためには、threadのQueueクラスを利用する必要があります。(引用先を参照ください。)
また、必ず shorn -> share! の順に実行されるのであれば、Mutexを導入すれば次のように解決できます。

class Sheep
  def initialize
    @shorn = false
    @mutex= Mutex.new
  end

  def shorn?
    @mutex.lock
    @shorn
  end

  def shear!
    puts "shearing..."
    @shorn = true
    @mutex.unlock
  end
end


色々と駄文を書いてきましたが、まとめますと、
・CRubyにおいて、C言語実装ライブラリ(Core Class、C拡張)はスレッドセーフで動作する。但し、GILの副次的な効果であり、今後どうなるかは分からない。
・一方で、Rubyで実装されたライブラリはスレッドセーフとならない可能性がある。
・ちなみに、Railsはスレッドセーフに動作できるので、Core Class + Railsが提供する関数は全てスレッドセーフで動作可能である。