Rubyでの非同期処理について

こんにちはhelloです。ISUCONで課題に上がっていたRubyでの非同期処理についてまとめてみたいと思います。

まず記事に関連しそうなワードについて整理したいと思います。

非同期処理について

  • 非同期処理とは、あるタスクが実行している際に他のタスクを実行できる方式です。

平行処理について

  • ある時点で一つの仕事しかしないが複数の仕事を切り替えて行う事。

並列処理について

  • ある時点で複数の仕事をしていること。

GIL(グローバルインタプリンタロック)

  • インタプリンタのスレッドによって保持されるスレッドセーフ出ないコードを他のスレッドと共有してしまうことを防ぐための排他ロック。

IO

  • DiskIO
  • NetworkIO
  • 同期ブロッキング
  • 同期ノンブロッキング
  • 非同期ブロッキング
    • システムコールでio処理を登録しレスポンスを待つ
    • 登録された何かが終わったらレスポンスを行う
    • epollが該当しO(1)で返される。
  • 非同期ノンブロッキング
  • マルチプロセスモデル
    • クライアントからの接続ごとにプロセスをフォークして処理を行う。
    • メモリ空間が子プロセス毎に独立している。
      • プロセス間でのメモリの直接参照が不可能。
  • マルチスレッド
    • 接続ごとにスレッドを生成する
    • メモリ空間を各スレッド間で共有可能
    • メモリ空間の切り替えがないため、メモリ消費量やコンテキストスイッチが抑えられる。
  • preforkモデル
  • workerモデル
    • マルチスレッドとマルチスレッドを合わせたもの。
    • プロセスが複数のスレッドをたち上げる。スレッドが一つのクライアントの処理を行う。
    • リクエストとスレッドが1対1
  • イベント駆動モデル
    • nginxやnodejsで利用されている
    • 1プロセスは1スレッドのみを利用する。
    • シングルスレッドのプロセスのためメモリ空間を共有可能
    • クライアントアクセスが増えてもプロセス・スレッド数は増えないため
    • メモリ消費量・コンテキストスイッチコストが小さい

非同期処理について整理していたつもりがすごい量になってしまいましたね...

Rubyでの非同期処理について

Delayedjob

  • queueをデータベースに用意し、定期的にポーリングする。
  • よくrailsで使われるやつですね。特に難しい事はないので割愛します。

    Rubyでのマルチプロセス

    Process.forkをすることによりプロセスを作成する事が出来ます。

port = #listenさせたいport
socket = TCPServer.new(port)
loop do
  read_socket = socket.accept
  fork do
    #何かしらの処理
  end
end

のような形になると思います。
gemとして有名なものとしてparallelがあります。Parallel.mapでそれぞれのプロセスで実行した結果をまとめる事ができます。
注意点としてあげるならメモリが共有されないのでparallelのようなgemを使用する際でも意識して下さい。結果を扱いたいときはParallel#mapなどを使ってください。

Rubyでのマルチスレッド

Thread.newをしてください

port = #listenさせたいport
socket = TCPServer.new(port)
loop do
  read_socket = socket.accept
  Thread.new do
    #何かしらの処理
  end
end
  • 基本的なRuby(CRuby)ではGVLがかかっているので、他の言語でいう並列処理を再現できていないです。
  • JRubyやRubinusでは並列処理をサポートしているのでPumaといった有名なwebサーバーはそれらを推奨しています。

RubyのIOについて

  • 上記で述べた同期ブロッキングを行うシステムコールを呼ぶメソッドはIO#write,IO#readなどが該当します。
  • ではRubyでノンブロッキングIOを行うには何を使うかというとこちらのメソッドとなります。IO#read_nonblock,IO#read_nonblock
  • これらのメソッドはO_NONBLOCKに設定しread, writeを行いノンブロッキングIOを提供してくれています。
  • (IO#read_partialはread+selectによるものらしい)
  • 非同期ブロッキングを提供するepollを行うメソッドがありません。
    • 理由などは調べれなかった...すいません
    • OSによってkqeueだったりepollだったりするのが起因しているのだろうか
  • しかしeventmachinenio4rというgemではepollを実装しているようですね。

Rubyでのイベント駆動について

  • 有名なものは上記であげたeventmachineというgemです。
  • reactorパターンを用いています。
  • reactorパターンとは読み込み書き込みが可能になったタイミングでシステムコールselect(2)またはepoll(2)を行い用意できたソケットから処理を進めていくものです。
  • pumaでもreactorパターンを用いている様子

他のRubyでのノンブロッキングIOについて

  • Asyncというgemを用いたパターンです。
  • このgemではFiberを用いてブロッキングIOを非同期的に操作します。
require 'async'
def test
  async do |task|
    #IO待ち処理
  end
end

Async do
  test
  test
  test
end

のような形で非同期処理を行える。内部の処理見るとAsyncで括った処理にたいして渡したブロックからFiberを生成し、生成したFiberをイベントループで監視しyield実行させているみたいですね。

  • Asyncを用いて作られたFalconは性能もよいらしくとてもきになります。

感想

  • 非同期って知らなきゃいけないことが多くとても難しいですね...
  • Ruby3ではGUILD?などがくるようなのでそれも楽しみにしていきたいと思います。

つたない記事でしたが読んで下さりありがとうございました。

参考URL