という、話だったそうじゃ

ニュートラルに生きるWebエンジニアの日記

Ruby FiberとかRactorを理解する上で理解しておく良さそうなこと

Ruby Kaigi 2020 TakeoutのMatzさんのキーノートで話されていた「Ractor」や「Fiber」、その話の中で出ていた「非同期I/O」について色々分かってないことが多かったので、前提知識として必要そうなことを纏めてみた

Matzさんが話していた内容

  • プログラミング言語の方向性を決めることは難しいということ。互換性を保ちながら良いものにする!
  • Ruby 3で目指す内容のコンセプトは、Faster(高速化) / More Productive(高い生産性)で、そのために ①Fast、②Concurrent、③Correct。つまり、早くて並列処理可能で正しくプログラムを書ける状況を提供していく

FiberやRactorは、Concurrentの話

並列化に関する話の中で、話される内容である。 そもそも、ソフトウェアを並列化する仕組みとして、

などの考え方で行われる。RubyはGILと呼ばれる グローバルインタプリタロック という考えの仕組みによって、並列で処理が行われることは無いのである。

蛇足だが、 ではなぜ、RubyがGILによって排他ロックを行っているかというと、おそらく...CRubyで使用されるライブラリがスレッドセーフではないためすぐ死んでしまうのだという理解である

本題に戻ると、Ruby 3のコンセプトとそのアクションである Concurrent は、この箇所に対する、いろいろな対処法を提供していくものである。

アイディア1 「非同期の I/O Fiber」

「実行が終わったら、ココに結果が格納されるよ」という、非同期によるI/OはJavascriptの世界ではPromiseやasync/awaitなどによって既に提供されていている。単にI/Oの多重化だけでは、マルチコアを活用しているとは言い切れないが、I/Oの比重が高いアプリの場合は、性能が出やすくなる。

Ruby 3ではそれを、 Fiber を使って非同期I/Oを提供するというアイディアである。ちなみにRuby 2では、ブロッキングI/Oであるとのことだが...。さていよいよ、わからなくなってた。ブロッキングI/O、非同期I/O?何が違うのだろう。

ブロッキングI/Oと同期I/O

ブロッキングI/Oとは、複数の処理のあるプログラムにおいて、I/Oの処理を待ってから進む

例えばread I/Oを行うシステムコールを行なったあと、そのシステムコールが処理を終えるまでユーザモードに戻る(コンテキストスイッチされる)という挙動で、コレを同期的、すわなち順番に1つずつやっていくものとなる

ノンブロッキングI/O

ノンブロッキングI/Oとは、例えば read I/O が行われたら、エラーが帰ってきて「マダ準備出来てないから後で来てな」と言わんばかりに、呼び出し元に再度問い合わせしてもらうことを想定しているのである

非同期I/O

ノンブロッキングI/Oと違うのは、再度、聞きに行くという処理を呼び出し元にさせないで、 準備できたから通知するね という形で、呼び出し元にシグナルやらコールバックやらで通知してあげてくれるのである。

非同期I/Oはファミレスの予約

非同期I/Oをあえて例えるなら、 近くのファミレスの予約である。ファミレスに到着したら、満員だったので予約システム(キオスク端末)に予約して、LINE連携したらLINEで呼び出し(通知)されるというもので、呼び出し元からすると多少は楽である。

ノンブロッキングI/Oは役所に提出した書類

ノンブロッキングI/Oをあえて例えるなら、 頑張って書類を整えて、やっとのことで役所に書類を提出したあとに、「まだですか?」という形で何度も窓口に聞きに行って「すみませんマダ終わってません」という、呼び出し元からすると少々手間のかかるやり方となる。 (最近だと、番号札を渡されて、呼び出してくれるので非同期I/Oかもしれない。)

しかし、両者の方法はどちらにしても、 エラー後から再度聞きに行く間の時間や、通知を待っている間 は別の処理を進められる出来るわけで、同期ブロッキングI/Oと比べると、他の処理を進められるぶん、ありがたい話なのである。

アイディア2 「Ractor」

マルチコアを活用するアイディアとしてのRactorである。CPUがボトルネックになるタイプの問題を解消していく。

複数のthreadでコードを動かすと、どのような問題が発生するのか。それは、状態(state)をスレッド同士が参照、共有し合う事によって、挙動がバグってしまうような事柄であり、そのためRactorは状態を共有しない方針となっている。

さて、実はこのような事柄。すなわち、スレッドセーフではない状況などにおいて、予期していない不整合が起きてしまうことには名前がついており、それを rase condition という。

race conditionってなんだろうか

各スレッドはバラバラに動く(非同期に動く)。その結果、処理した順番等によって結果が変化してしまう状態のことを race condition という。複数スレッドで共有された変数を叩く場合など、 data race という言葉もある。これは、同じ箱を同時に処理してしまって、どういう結果になっているのか全く想像ができないことを指す。

さて、本題に戻るとする。ではRactorは何も共有できないかというと、そうではない。以下の3つは共有できるのである。

  • Immutable Object
  • Deeply Frozen Objects (変数が指すオブジェクトに対してもFrozenさせる)
  • Class / Module

など、要は 値が書き換え不可能になっているもののみ 共有することが出来る。ClassやModuleはオープンクラスであるRubyにとって、Immutableではないため一見「状態が変わりえるやん...」と思うかもしれない。しかし、Lock(コレはわからん)という仕組みによって、ClassやModuleをFrozenすれば共有しあえるようになるようである。