select v.s. epoll

select 及び select とよく対比される epoll を調べた。

Ruby IO.select

Ruby の IO.selectselect(2) を呼んでる。

IO.select (Ruby 3.1 リファレンスマニュアル)

select(2) を実行します。

与えられた入力/出力/例外待ちの IO オブジェクトの中から準備ができたものをそれぞれ配列にして、配列の配列として返します。タイムアウトした時には nil を返します。

Multiplexing Connections - Working With… より引用

# Given an Array of connections.
connections = [<TCPSocket>, <TCPSocket>, <TCPSocket>]

loop do
  # Ask select(2) which connections are ready for reading.
  ready = IO.select(connections)

  # Read data only from the available connections.
  readable_connections = ready[0]
  readable_connections.each do |conn|
    data = conn.readpartial(4096)
    process(data)
  end
end

Ruby epoll

The poll(2) system call provides some slight differences over select(2) but is more or less on par. The (Linux) epoll(2) and (BSD) kqueue(2) system calls provide a more performing, modern alternative to select(2) and poll(2). For example, a high-performance networking toolkit like EventMachine will favour epoll(2) or kqueue(2) where possible.

上記事でも引用されているように、 epoll を ruby で使いたければ、ActionCable や puma 内部でもつかわれている nio4r gem を使うのがよい。

計算量の比較

  • select: ファイルディスクリプタを一つ一つ見にいくので O(n) の計算量
  • epoll: カーネルでファイルディスクリプタのステータスを管理してくれてどのファイルディスクリプタがreadyかをカーネルが返してくれるので O(1) の計算量

ノンブロッキングI/Oと非同期I/Oの違いを理解する

epoll API は Linux カーネル 2.5.44 に導入されたもので、2.6以降なら利用可能。epollはファイルディスクリプタの数に制限が無いのに加えて、ファイルディスクリプタの状態変化監視も改善されている。具体的には,ファイルディスクリプタの状態をカーネルで監視しており、変化したものを直接通知できるためselect、pollの時のようにループを使った監視をする必要がない。これにより、計算量はO(1)となり、よりパフォーマンスの高い多重化I/Oを実現できる。

まとめると下記のようになる。epollが一番モダンでパフォーマンスが良い。

epoll > poll > select

See also. Rubyで学ぶWebサーバーアーキテクチャ(Preforking, ThreadPool, イベント駆動モデル) - Qiita

Go の epoll

epoll使用の流れとしては以下のようになります。

  1. epoll_create1関数でepollインスタンスを作り、返り値としてそのインスタンスのfdを受け取る
  2. epoll_ctl関数で、epollの監視対象のfdを編集する
  3. epoll_wait関数で、監視対象に何かイベントが起こっていないかをチェックする

ref. Goランタイムケーススタディ|Goでの並行処理を徹底解剖!

参考

ネイティブスレッド v.s. グリーンスレッド

スレッドには大きく分けて2つの種類があります。

  • ネイティブスレッド
  • グリーンスレッド

ネイティブスレッドはOSが提供するスレッドで、グリーンスレッドはOSではなく言語や仮想マシンが提供するスレッドです。

Ruby の場合

ネイティブスレッドを用いて実装されていますが、現在の実装では Ruby VM は Giant VM lock (GVL) を有しており、同時に実行されるネイティブスレッドは常にひとつです。ただし、IO 関連のブロックする可能性があるシステムコールを行う場合には GVL を解放します。その場合にはスレッドは同時に実行され得ます。

class Thread (Ruby 3.1 リファレンスマニュアル)

追記: