はじめに
CrystalのFiberのリスケジューリングがどう動いているのか気になって、軽く調べてみました。
といってもChatGPTを使って、Crystal本体のコードを読んでもらった結果をまとめたものですが、あとから簡単に振り返られるように記録に残したいのでQiita記事にします。
Fiberのコードを見る
まず、fiber.cr
を読んでみます。
ここで、Fiberの実行はCrystal::Scheduler
によって管理されていることが分かりました。そこで、次にscheduler.cr
を確認します。
調べてみると、**Fiber.swapcontext
**で現在のFiberの実行コンテキストを保存し、別のFiberを再開する仕組みがあることがわかります。では、このswapcontext
はどこに実装されているのでしょうか?
Fiberのコンテキスト切り替え
fiber/context
ディレクトリに各プラットフォーム向けの実装が用意されています。
fiber
├── context
│ ├── aarch64.cr
│ ├── arm.cr
│ ├── i386.cr
│ ├── interpreted.cr
│ ├── wasm32.cr
│ ├── x86_64-microsoft.cr
│ └── x86_64-sysv.cr
├── context.cr
└── stack_pool.cr
x86_64-sysv.cr
は、Linuxで使用されるSystem V ABI用の実装です。ここでは、アセンブリを使ってレジスタやスタックを直接操作しています。
2つの重要な関数
-
makecontext
:Fiberが最初に実行されるエントリーポイントをfiber_main
に設定します。 -
swapcontext
:スタックポインタを切り替えて、現在のFiberから新しいFiberに実行コンテキストを移します。
低レベルの実装は理解困難ですが、ざっくりとこのような仕組みで動いていることが分かりました。
スタックプールの仕組み
次に、stack_pool.cr
についても確認します。
ここでは、各Fiberごとに8 MiBのスタックメモリが割り当てられ、効率的に管理されています。スタックの再利用や解放を通じて、メモリの使用量を抑える仕組みになっています。
スケジューラが次のFiberを選ぶ仕組み
次に、Crystalのスケジューラがどのように次のFiberを割り当てるかを見ていきます。以下がreschedule
メソッドの実装です。
protected def reschedule : Nil
loop do
if runnable = @lock.sync { @runnables.shift? }
resume(runnable) unless runnable == @thread.current_fiber
break
else
Crystal.trace :sched, "event_loop" do
@event_loop.run(blocking: true)
end
end
end
end
-
実行可能なFiberがある場合、キュー(
@runnables
)から取得してresume
で再開します。 - 実行可能なFiberがない場合、
@event_loop.run(blocking: true)
が呼ばれ、次のイベントが発生するまでブロッキングされます。
EventLoopとlibeventの関係
@event_loop
は、CrystalのEventLoopです。
Linuxでは、Crystal::LibEvent::EventLoop
が使用されており、これはlibeventのバインディングです。
libevent
は非同期IOやタイムアウトを管理するC言語のライブラリで、Crystalはこの仕組みを使ってイベントを効率的に監視しています。
libevent
は、Linuxでepollのようなシステムコールを使ってイベントを監視します。
CrystalのLibEvent2.event_base_loop
は、イベントが発生するまで待機します。例えば、epoll_wait
を使うことで、ソケットやファイルディスクリプタの状態変化を監視し、イベントが発生すると対応するFiberを再開します。