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だったりするのが起因しているのだろうか
- しかしeventmachineやnio4rというgemでは
epoll
を実装しているようですね。
Rubyでのイベント駆動について
- 有名なものは上記であげたeventmachineというgemです。
- reactorパターンを用いています。
- reactorパターンとは読み込み書き込みが可能になったタイミングでシステムコール
select(2)
またはepoll(2)
を行い用意できたソケットから処理を進めていくものです。 - pumaでもreactorパターンを用いている様子
他のRubyでのノンブロッキング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
- https://qiita.com/legokichi/items/1f3b1bd51e206ffdd2a6
- https://blog.takanabe.tokyo/2015/03/%E3%83%8E%E3%83%B3%E3%83%96%E3%83%AD%E3%83%83%E3%82%AD%E3%83%B3%E3%82%B0i/o%E3%81%A8%E9%9D%9E%E5%90%8C%E6%9C%9Fi/o%E3%81%AE%E9%81%95%E3%81%84%E3%82%92%E7%90%86%E8%A7%A3%E3%81%99%E3%82%8B/
- https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%AD%E3%83%BC%E3%83%90%E3%83%AB%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%97%E3%83%AA%E3%82%BF%E3%83%AD%E3%83%83%E3%82%AF
- https://engineer.recruit-lifestyle.co.jp/techblog/2019-12-13-node-async-io/