@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Making *MaNy* threads on Ruby

rubykaigi.org

発表資料です。

twitter.com

この記事を書くために発表資料を読み直しました。 MaNyではスレッドがM:Nになるんだと勘違いしていたことに気がつきました。

スレッドは1:Nでした。 順番逆かな?N:1でした。 複数のRubyのスレッドが一つのネイティブスレッドで動くようになります。 要するにRuby1.8までのグリーンスレッドと同じ方式です。 先祖帰りです。

RubyのスレッドにはGVLがあります。 Concurrentには動いてもParallelには動きません。 であれば、Rubyのスレッドひとつひとつにネイティブスレッドを割り当てなくてもいいはずです。 ネイティブスレッド一つにうまいことディスパッチできれば、OSの資源を節約できます。

なぜRuby1.9では、Rubyのスレッドとネイティブスレッドを1:1にしたのでしょうか? C拡張を使ったGemの中で長時間かかる計算を実行すると、RubyVMからは検知できません。 Rubyのスレッドを一定時間で切り替えることができません*1。 一つでも時間を占有するRubyのスレッドがあるとすべてのRubyのスレッドが遅くなります。

イベントループと同じ問題がおきるのが面白いです。 僕はずっとGVLがある理由は、Mutexを使ってとってなくてレースコンディションを起こしちゃうC拡張Gemがある(あり得る)からだと思ってました。 そうじゃない、あるいは、それだけじゃないんですね。

時間を占有するRubyスレッドの問題を解決するために、Dedicated Native thread(占有ネイティブスレッド)というマークを入れるそうです。 そういうマークができるAPIをつくるって意味なんですかね? あんまりよくわかっていないです。 Ruby1.9以降でGVLをリリースするAPIがあって、これはよく使われるようになっているので、同じ要領でAPIを用意すれば、回避出来るだろうということなのでしょうか?

占有ネイティブスレッドがマークされた場合は、Ruby VMがネイティブスレッドを増やします。 占有ネイティブスレッドをマークした以外のRubyスレッドを新しいネイティブススレッドでN:1で動かします。 占有ネイティブスレッドをマークしたRubyスレッドは、元のネイティブスレッドを使って、現在と同じように1:1で動かすそうです。 これで今のRubyのスレッドと同じような動きを担保するそうです。

MaNyでM:NになるのはRactorだそうです。 複数のRuby Ractorに対して、複数のネイティブスレッドが動くそうです。 ここがgolangのgoroutineがM:Nで動くのと同じイメージみたいです。 Concurrentに動かしたければスレッドをつかう、Parallelに動かしたければRactorをつかうという、今のプログラミングパラダイムから変更はないようです。 僕はなぜか、いつの間にかスレッドがParallelに動くって勘違いしてました。

スレッドをつくるプログラムを書くと、並行数に上限を設けたくなります。 リクエストに対して5スレッドつくるプログラムでは、リクエストが100個来ると500スレッドできます。 CPUのコアが16個のときに500スレッドも作っても無駄です。 スレッド数の上限を10とか20とかつけたくなります。 それでスレッドプール作ってキュー使ってワーカーモデルで実装してってなります。 MaNyでは、この部分をRuby VMがやってくれるという話みたいです。

そういえばgoroutineが便利なのも、OSのスレッド数が何個になるか気にせず、バンバンgoroutine作れば、go-langが良い感じに並列に動かしてくれる点でした。 Rubyみたいな歴史のある言語でこれをやろうとすると、どえらく大変なんですね・・・。ひえー

*1:僕は、この原因をよく理解していません。検知できないのか、割り込めるタイミングがわからないのか、それ以外の理由があるのかも知れません