Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18359] ファイルの概要

このコミットは、GoランタイムのスケジューラにおけるP(プロセッサ)のリテイク(再取得)ロジックの調整に関するものです。特に、GOMAXPROCSが1より大きい場合に、システムコール中のPが適切に再取得されず、sysmon(システムモニター)スレッドの深いスリープを妨げていた問題に対処しています。これにより、特定の条件下でのCPU消費が削減されます。

コミット

commit 179d41feccc29260d1a16294647df218f1a6746a
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Jan 27 23:17:46 2014 +0400

    runtime: tune P retake logic
    When GOMAXPROCS>1 the last P in syscall is never retaken
    (because there are already idle P's -- npidle>0).
    This prevents sysmon thread from sleeping.
    On a darwin machine the program from issue 6673 constantly
    consumes ~0.2% CPU. With this change it stably consumes 0.0% CPU.
    Fixes #6673.
    
    R=golang-codereviews, r
    CC=bradfitz, golang-codereviews, iant, khr
    https://golang.org/cl/56990045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/179d41feccc29260d1a16294647df218f1a6746a

元コミット内容

このコミットは、GoランタイムのP(プロセッサ)のリテイクロジックを調整するものです。GOMAXPROCSが1より大きい場合、システムコール中の最後のPが再取得されないことがあり、これがsysmonスレッドのスリープを妨げ、結果としてCPUを消費し続ける問題を引き起こしていました。この変更により、sysmonスレッドが適切にスリープできるようになり、CPU消費が削減されます。特に、Issue 6673で報告されたDarwinマシンでの問題(0.2%のCPU消費)が0.0%に改善されたと述べられています。

変更の背景

Goランタイムのスケジューラは、G(Goroutine)、M(Machine/OSスレッド)、P(Processor/論理プロセッサ)という3つのエンティティを管理しています。PはMにアタッチされ、Gを実行するためのコンテキストを提供します。GOMAXPROCSは、同時に実行できるPの最大数を制御します。

このコミットの背景には、GOMAXPROCS > 1 の環境下で、システムコール(syscall)に入ったPが、他のアイドルなPが存在する場合に、スケジューラによって適切に再取得されないという問題がありました。Goランタイムにはsysmon(システムモニター)というバックグラウンドスレッドが存在し、これはガベージコレクションのトリガー、プリエンプションの実行、ネットワークポーラーの呼び出しなど、様々なハウスキーピングタスクを担当します。sysmonスレッドは、これらのタスクがない間はスリープすることでCPUリソースを節約します。

しかし、システムコール中のPが再取得されないと、sysmonスレッドがPを解放できず、結果としてsysmonスレッドが深いスリープに入れず、CPUを消費し続けるという非効率な状態が発生していました。これは、特にアイドル状態のプログラムにおいて顕著な問題となり、Issue 6673として報告されていました。このコミットは、この非効率性を解消し、CPUリソースの利用効率を向上させることを目的としています。

前提知識の解説

  • Goスケジューラ (GMPモデル):
    • G (Goroutine): Goの軽量スレッド。Goプログラムの並行実行の単位。
    • M (Machine): OSスレッド。Goroutineを実行するための実際のOSスレッド。
    • P (Processor): 論理プロセッサ。MがGを実行するために必要なコンテキスト(ローカルな実行キューなど)を提供する。GOMAXPROCS環境変数によって、同時に実行可能なPの数が決まる。
  • システムコール (syscall): プログラムがOSの機能(ファイルI/O、ネットワーク通信など)を呼び出すこと。システムコール中は、Mはブロックされる可能性がある。
  • Pのリテイク (retake): Goスケジューラが、システムコールなどでブロックされているPを他のMに再割り当てしたり、Pのコンテキストを解放したりするプロセス。これにより、他のGoroutineが実行できるようになる。
  • sysmon (システムモニター): Goランタイムのバックグラウンドスレッド。定期的に起動し、以下のようなタスクを実行する。
    • 長時間実行されているGoroutineのプリエンプション(強制中断)
    • ネットワークポーラーの呼び出し
    • ガベージコレクションのトリガー
    • アイドルなMのスピン状態の監視
    • Pのリテイクの補助 sysmonは、これらのタスクがない間はスリープすることでCPUリソースを節約する。
  • npidle: アイドル状態のPの数。
  • nmspinning: スピン状態(Goroutineを探している状態)のMの数。
  • p->runqhead == p->runqtail: Pのローカル実行キューが空であることを示す。つまり、そのPには実行すべきGoroutineがない状態。
  • pd->syscalltickpd->syscallwhen: Pがシステムコールに入った時刻を追跡するための内部変数。syscallticksysmonのティック数、syscallwhenは絶対時刻(ナノ秒)を記録する。

技術的詳細

このコミットの変更は、src/pkg/runtime/proc.c内のretake関数に集中しています。retake関数はsysmonスレッドによって定期的に呼び出され、システムコールに入っているPを監視し、必要に応じて再取得を試みます。

変更前のロジックでは、システムコール中のPを再取得するかどうかの判断において、以下の条件がありました。

if(p->runqhead == p->runqtail &&
	runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) > 0)
	continue;

この条件は、「もしPのローカル実行キューが空であり(p->runqhead == p->runqtail)、かつ、スピンしているMまたはアイドルなPが存在する場合(nmspinning + npidle > 0)は、このPを再取得せずにスキップする」という意味です。

このロジックの問題点は、GOMAXPROCS > 1 の環境で、もし他のPがアイドル状態(npidle > 0)であれば、システムコールに入ったPがいつまでも再取得されない可能性があることでした。特に、プログラムがアイドル状態にあり、実行すべきGoroutineがほとんどない場合、システムコールから戻ったPがすぐにアイドル状態になり、npidleが0より大きい状態が続くため、sysmonがそのPを再取得する機会を失っていました。これにより、sysmonスレッドがPを解放できず、深いスリープに入れずにCPUを消費し続けるという問題が発生していました。

このコミットでは、上記の条件に新しい句が追加されました。

if(p->runqhead == p->runqtail &&
	runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) > 0 &&
	pd->syscallwhen + 10*1000*1000 > now) // 追加された条件
	continue;

追加された条件 pd->syscallwhen + 10*1000*1000 > now は、「Pがシステムコールに入ってから10ミリ秒(10 * 1000 * 1000ナノ秒)以上経過していない場合」を意味します。

この変更により、Pのローカルキューが空で、かつ他のアイドルなPが存在する場合でも、システムコールに入ってから10ミリ秒以上経過していれば、そのPは再取得の対象となります。つまり、たとえ他のアイドルなPが存在していても、システムコール中のPが無限に再取得されない状態を防ぎ、最終的にはsysmonスレッドがPを解放し、深いスリープに入れるように修正されました。

この「10ミリ秒」という閾値は、システムコールから戻ったPがすぐにアイドル状態になったとしても、ある程度の時間が経過すれば強制的に再取得されるようにするためのものです。これにより、sysmonスレッドが不必要にCPUを消費し続ける状況が解消されます。

コアとなるコードの変更箇所

src/pkg/runtime/proc.c ファイルの retake 関数内の条件分岐が変更されました。

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -2534,16 +2534,19 @@ retake(int64 now)
 		pd = &pdesc[i];
 		s = p->status;
 		if(s == Psyscall) {
-			// Retake P from syscall if it's there for more than 1 sysmon tick (20us).
-			// But only if there is other work to do.
+			// Retake P from syscall if it's there for more than 1 sysmon tick (at least 20us).
 			t = p->syscalltick;
 			if(pd->syscalltick != t) {
 				pd->syscalltick = t;
 				pd->syscallwhen = now;
 				continue;
 			}
+			// On the one hand we don't want to retake Ps if there is no other work to do,
+			// but on the other hand we want to retake them eventually
+			// because they can prevent the sysmon thread from deep sleep.
 			if(p->runqhead == p->runqtail &&
-			runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) > 0)
+			runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) > 0 &&
+			pd->syscallwhen + 10*1000*1000 > now)
 				continue;
 			// Need to decrement number of idle locked M's
 			// (pretending that one more is running) before the CAS.

コアとなるコードの解説

変更の中心は、retake関数内のPsyscall状態のPを処理する部分です。

  1. コメントの変更: 変更前: // Retake P from syscall if it's there for more than 1 sysmon tick (20us). // But only if there is other work to do. 変更後: // Retake P from syscall if it's there for more than 1 sysmon tick (at least 20us). // On the one hand we don't want to retake Ps if there is no other work to do, // but on the other hand we want to retake them eventually // because they can prevent the sysmon thread from deep sleep.

    このコメントの変更は、このコミットの意図を明確にしています。以前は「他の作業がある場合にのみ再取得する」というニュアンスでしたが、変更後は「他の作業がない場合でも最終的には再取得するべきである。なぜなら、そうしないとsysmonスレッドの深いスリープを妨げるからだ」という、より積極的な再取得の必要性が強調されています。

  2. 条件式の追加: 変更前:

    if(p->runqhead == p->runqtail &&
    	runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) > 0)
    	continue;
    

    変更後:

    if(p->runqhead == p->runqtail &&
    	runtime·atomicload(&runtime·sched.nmspinning) + runtime·atomicload(&runtime·sched.npidle) > 0 &&
    	pd->syscallwhen + 10*1000*1000 > now) // この行が追加
    	continue;
    

    この追加された条件 pd->syscallwhen + 10*1000*1000 > now がこのコミットの核心です。

    • pd->syscallwhen: Pがシステムコールに入った時刻(ナノ秒単位)。
    • 10*1000*1000: 10ミリ秒をナノ秒で表現したもの。
    • now: 現在時刻(ナノ秒単位)。

    この条件がtrueである間(つまり、システムコールに入ってから10ミリ秒が経過していない間)は、以前の条件(ローカルキューが空で、アイドルなPやスピン中のMがいる)が満たされていればcontinue(スキップ)します。 しかし、pd->syscallwhen + 10*1000*1000 > nowfalseになった場合、つまりシステムコールに入ってから10ミリ秒以上が経過した場合は、たとえローカルキューが空で他のアイドルなPがいても、このifブロックは実行されず、Pは再取得の対象となります。

    これにより、システムコール中のPが他のアイドルなPの存在によって無限に再取得されないという状況が解消され、sysmonスレッドがPを解放して深いスリープに入れるようになります。これは、アイドル状態のGoプログラムにおけるCPU消費の削減に直接貢献します。

関連リンク

参考にした情報源リンク

  • Goのスケジューラに関するドキュメントやブログ記事(一般的なGMPモデルの解説)
  • Goランタイムのソースコード(src/runtime/proc.gosrc/runtime/proc.c の関連部分)
  • Goのsysmonに関する解説記事
  • GoのIssueトラッカー(Issue 6673の詳細)
  • Goのコードレビューシステム(CL 56990045の詳細)