Goのスケジューラー: Ms, Ps & Gs

基礎

Goのランタイムはスケジューリング、ガベージコレクションとgoroutineのランタイム環境を管理しています。ここではスケジューラーのみに焦点を当てることにします。

ランタイムジューラーはgoroutineをOSのスレッドにマッピングして実行します。goroutineは軽量なスレッドで大変少ないコストで起動できます。それぞれのgoroutineはGとよばれる構造体で定義されており、スタックの情報と現在の状態を持つフィールドを含んでいます。なので G = goroutine です。

ランタイムは各G を追跡しており、それらP と名付けられている論理プロセッサにマップしています。P は抽象的なリソース、またはコンテキストとして見ることができ、OSスレッド (マシンまたはMと呼ばれる)G を実行します。

runtime.GOMAXPROCS(numLogicalProcessors) を呼び出すことでランタイムの論理プロセッサの数を制御できますが、もしこのパラメータを微調整したい場合 (殆どの場合すべきではありません)は設定をして忘れてください。というのも、GCを停止する必要があるためです。

本質的にOSはスレッドを動かしあなたのコードを動かしています。Go の細工ではコンパイラが様々な場所でGoランタイムの呼び出しを挿入しているため(例えばチャネルによる値の送信や、ランタイムパッケージの呼び出しなど)、Goはスケジューラーに知らせ実行できるのです。

M, P, G のダンス

M、P、G間の相互作用は少し複雑です。Gao Chao氏によるランタイムスケジューラーのスライドから抜粋したこの素晴らしいワークフローグラフをみてください。
みて分かる通り、G に関しての2つのタイプのキューがあります。schedy構造体のグローバルキュー(めったに使われない)と各P は実行可能なG のキューを管理しています。

goroutineを実行するために、MP のコンテキストを保持している必要があります。マシンはそれからP ルーチンのgoroutineをポップしてコードを実行します。

あたらしいgoroutine (go func()  を呼び出す)をスケジュールすると、P のキューに配置されます。この興味深いワークスティールスケジューリングアルゴリズムはMG のコードの実行を完了させ、次に空のG から別のG を取り除こうとしたときに実行されます。その後、ランダムに別のP を選択し、そこから実行可能なGの半分を盗もう とします。

興味深いことに、goroutineはブロックするシステムコールを発生します。実行するGがある場合、ブロッキングシステムコールはインターセプトされ、ランタイムはPからスレッドを切り離し、そのプロセッサにサービスを提供するために新しいOSスレッドを作成します。(アイドルスレッドが存在しない場合)

システムコールが再開すると、goroutineはローカルランキューに戻し、スレッドは自分自身を停止させ(スレッドが実行されていないということを意味する) 、アイドル状態のスレッドのリストに自分自身を挿入します。

もし、goroutineがネットワークコールをすると、ランタイムは同様の動きをします。呼び出しはインターセプトされますが、Goには独自のスレッドを持つネットワークポーラーが統合されているため割当が行われます。

特に、Goのランタイムは現在のgoroutineがブロックされても異なるgorutineが動きます。
  • ブロッキングシステムコール (例えばファイルを開く)
  • ネットワーク入力
  • チャネル操作
  • syncパッケージのプリミティブ

スケジューラートレーシング

Goはランタイムスケジューラーをトレースできるようになっています。これは GODEBUG 環境変数で利用できます。

$ GODEBUG=scheddetail=1,schedtrace=1000 ./program

ここに環境変数を指定したときの出力例を載せておきます。
SCHED 0ms: gomaxprocs=8 idleprocs=7 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0
  P0: status=1 schedtick=0 syscalltick=0 m=0 runqsize=0 gfreecnt=0
  P1: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  P2: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  P3: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  P4: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  P5: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  P6: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  P7: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0
  M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1
  M0: p=0 curg=1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=1