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が提供する関数は全てスレッドセーフで動作可能である。