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

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

このコミットは、GoランタイムのスケジューラにおけるゴルーチンとM(OSスレッド)間の関連付けの管理方法をリファクタリングするものです。具体的には、dropgという新しいヘルパー関数を導入し、schedule関数がロックされたゴルーチン(lockedg)の処理をより適切に行うように変更することで、コードの堅牢性と保守性を向上させています。

コミット

commit 64c2083ebc4a071f842368154c77601e9463f5d5
Author: Russ Cox <rsc@golang.org>
Date:   Mon Jul 14 20:56:37 2014 -0400

    runtime: refactor routines for stopping, running goroutine from m
    
    This CL adds 'dropg', which is called to drop the association
    between m and its current goroutine, and it makes schedule
    handle locked goroutines correctly, instead of requiring all
    callers of schedule to do that.
    
    The effect is that if you want to take over an m for, say,
    garbage collection work while still allowing the current g
    to run on some other m, you can do an mcall to a function
    that is:
    
            // dissociate gp
            dropg();
            gp->status = Gwaiting; // for ready
    
            // put gp on run queue for others to find
            runtime·ready(gp);\
    
            /* ... do other work here ... */
    
            // done with m, let it run goroutines again
            schedule();
    
    Before this CL, the dropg() body had to be written explicitly,
    and the check for lockedg before schedule had to be
    written explicitly too, both of which make the code a bit
    more fragile than it needs to be.
    
    LGTM=iant
    R=dvyukov, iant
    CC=golang-codereviews, rlh
    https://golang.org/cl/113110043

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

https://github.com/golang/go/commit/64c2083ebc4a071f842368154c77601e9463f5d5

元コミット内容

このコミットは、Goランタイムにおいて、M(OSスレッド)から現在のG(ゴルーチン)を切り離すルーチンをリファクタリングするものです。具体的には、dropgという新しい関数を追加し、Mと現在のゴルーチンとの関連付けを解除できるようにします。また、schedule関数がロックされたゴルーチン(lockedg)を正しく処理するように変更し、scheduleの呼び出し元がその処理を行う必要がなくなりました。

これにより、例えばガベージコレクション(GC)作業のためにMを一時的に占有しつつ、現在のゴルーチンを別のMで実行させたい場合などに、より簡潔で堅牢なコードを書くことが可能になります。以前は、dropg()の本体やscheduleを呼び出す前のlockedgのチェックを明示的に記述する必要があり、コードが脆弱になる原因となっていました。

変更の背景

Goランタイムのスケジューラは、M(OSスレッド)、P(論理プロセッサ)、G(ゴルーチン)という3つの主要なエンティティを管理しています。MはOSスレッドを表し、GはGoの並行処理の単位であるゴルーチンを表します。PはMがGを実行するために必要なリソース(コンテキスト)を提供します。通常、MはPを介してGを実行します。

しかし、特定のシナリオ、特にランタイム内部の低レベルな操作(例:ガベージコレクション、Cgo呼び出し、runtime.LockOSThreadによるOSスレッドのロック)では、Mが一時的に特定のゴルーチンから切り離されたり、あるいは特定のゴルーチンにロックされたりする状況が発生します。

このコミット以前は、MとGの関連付けを解除するロジックや、Mが特定のゴルーチンにロックされている(lockedg)場合の処理が、ランタイム内の複数の場所で重複して記述されており、コードの重複と脆弱性を招いていました。特に、schedule関数を呼び出す前にlockedgのチェックを各呼び出し元で行う必要があり、これはエラーの温床となる可能性がありました。

このリファクタリングの目的は、これらの共通のロジックを抽象化し、一元化することで、ランタイムコードの保守性を高め、バグのリスクを減らすことにありました。特に、GCのような重要なランタイム操作が、ゴルーチンのスケジューリングに与える影響をより安全かつ効率的に管理できるようにすることが狙いです。

前提知識の解説

このコミットを理解するためには、Goランタイムのスケジューラとゴルーチンのライフサイクルに関する基本的な知識が必要です。

Goスケジューラ (M, P, G)

  • G (Goroutine): Goにおける並行実行の単位です。軽量なスレッドのようなもので、Goプログラムの関数呼び出しとして実行されます。Goランタイムによってスケジューリングされます。
  • M (Machine): OSスレッドを表します。Goランタイムは、OSスレッドをMとして抽象化し、その上でゴルーチンを実行します。MはOSのスケジューラによって管理されます。
  • P (Processor): 論理プロセッサを表します。MがGを実行するために必要なコンテキスト(ローカルな実行キュー、スケジューラの状態など)を提供します。Pの数は通常、GOMAXPROCS環境変数によって制御され、CPUコア数に等しいことが多いです。MはPを取得して初めてGを実行できます。

Goスケジューラは、M-P-Gモデルに基づいて動作します。MはPを取得し、PのローカルキューからGを取り出して実行します。Gがシステムコールなどでブロックされると、MはPを解放し、別のMがそのPを取得して他のGを実行できます。

ゴルーチンの状態

ゴルーチンは、そのライフサイクルにおいて様々な状態を取ります。このコミットに関連する主な状態は以下の通りです。

  • Grunning: ゴルーチンがM上で実行中である状態。
  • Grunnable: ゴルーチンが実行可能であり、PのローカルキューまたはグローバルキューでMにピックアップされるのを待っている状態。
  • Gwaiting: ゴルーチンが何らかのイベント(例:ネットワークI/O、タイマー、チャネル操作)を待っている状態。この状態のゴルーチンは実行可能ではありません。

lockedg (ロックされたゴルーチン)

Goランタイムには、特定のMにゴルーチンを「ロック」する機能があります。これは主に、Cgo呼び出し(C言語のコードを呼び出す場合)や、runtime.LockOSThread()関数を使用して、現在のゴルーチンが特定のOSスレッド(M)上で排他的に実行されることを保証する必要がある場合に用いられます。lockedgは、Mが現在ロックされているゴルーチンを指すポインタです。lockedgが設定されているMは、そのlockedg以外のゴルーチンを実行してはなりません。

mcall

mcallは、Goランタイムの非常に低レベルな関数で、現在のゴルーチンのスタックからMのシステムスタックに切り替えるために使用されます。これは、ゴルーチンのコンテキストから離れて、ランタイムの内部処理(例:スケジューラの操作、GC)を行う必要がある場合に用いられます。mcallが呼び出されると、現在のゴルーチンは一時的に中断され、Mはゴルーチンとは独立した処理を実行できるようになります。

ガベージコレクション (GC) とスケジューラ

Goのガベージコレクタは、メモリ管理の重要な部分です。GoのGCは、初期のバージョンでは「Stop-the-World (STW)」フェーズを持っていました。STWフェーズでは、すべてのゴルーチンの実行が一時的に停止され、GCが安全にメモリをスキャンし、マーク&スイープなどの処理を行います。このSTWフェーズ中、ランタイムのMはGC作業に専念する必要があり、通常のゴルーチン実行から切り離される必要があります。このコミットで導入されるdropgのようなメカニズムは、このようなSTWフェーズやその他のランタイム内部処理において、MとGの関連付けを一時的に解除するために利用されます。

技術的詳細

このコミットの技術的な核心は、GoランタイムのスケジューラにおけるMとGの関連付けの管理を、より抽象的で堅牢な方法に移行した点にあります。

dropg 関数の導入

以前は、Mから現在のゴルーチン(g->m->curg)を切り離す必要がある場合、g->m->curg->m = nil;g->m->curg = nil; のようなコードを各所で明示的に記述する必要がありました。このコミットでは、この共通のロジックをdropgという新しいヘルパー関数にカプセル化しました。

dropgの定義は以下の通りです。

void
dropg(void)
{
    if(g->m->lockedg == nil) { // Mが特定のゴルーチンにロックされていない場合のみ
        g->m->curg->m = nil;   // 現在のゴルーチンからMへの参照を解除
        g->m->curg = nil;      // Mから現在のゴルーチンへの参照を解除
    }
}

この関数は、Mがlockedg(特定のゴルーチンにロックされている状態)でない場合にのみ、Mと現在のゴルーチン(g->m->curg)の関連付けを解除します。lockedgである場合は、Mはそのゴルーチンに専念しているため、関連付けを解除すべきではありません。

このdropgの導入により、park0runtime·gosched0goexit0exitsyscall0といった複数の関数で重複していたMとGの関連付け解除ロジックがdropg()の呼び出しに置き換えられ、コードの簡潔性と保守性が向上しました。

schedule 関数における lockedg の一元的な処理

このコミットのもう一つの重要な変更点は、schedule関数がlockedgをより適切に処理するようにリファクタリングされたことです。

以前は、scheduleを呼び出す前に、呼び出し元がMがlockedgであるかどうかをチェックし、もしそうであれば、scheduleを呼び出さずにlockedgを再実行するロジックを明示的に記述する必要がありました。これは、schedulelockedgを考慮せずに他のゴルーチンをスケジューリングしてしまうことを防ぐためです。

このコミットでは、schedule関数の冒頭に以下のチェックが追加されました。

if(g->m->lockedg) {
    stoplockedm();           // ロックされたMを停止(必要であれば)
    execute(g->m->lockedg);  // ロックされたゴルーチンを実行(決して戻らない)
}

これにより、scheduleが呼び出された際に、Mがlockedgである場合は、scheduleは他のゴルーチンを探索する代わりに、直ちにそのlockedgを再実行するようになりました。この変更により、scheduleの呼び出し元はlockedgのチェックを行う必要がなくなり、コードの重複が解消され、scheduleの利用がより安全になりました。

ガベージコレクションシナリオへの応用

コミットメッセージで言及されているように、このリファクタリングはガベージコレクションのようなシナリオで特に有用です。GCがSTWフェーズに入る際、GCワーカーはMを一時的に占有してGC作業を行います。このとき、Mが以前実行していたゴルーチンは、GC作業が完了するまで中断されるか、あるいは別のMに移行される必要があります。

新しいdropgscheduleの動作により、GCワーカーは以下のような手順でMを占有し、ゴルーチンを切り離すことができます。

  1. mcallを使用して、GC作業を行う関数にMのコンテキストを切り替えます。
  2. GC作業関数内でdropg()を呼び出し、Mと以前のゴルーチン(gp)の関連付けを解除します。
  3. gp->status = Gwaiting; のようにゴルーチンの状態を更新し、runtime·ready(gp); を呼び出して、gpを他のMが実行できるように実行キューに戻します。
  4. GC作業を実行します。
  5. GC作業が完了したら、schedule()を呼び出して、Mが再び通常のゴルーチンをスケジューリングできるようにします。

この流れにより、GC作業中にMがゴルーチンから切り離され、GC作業が完了した後にMがスムーズにスケジューリングに戻れるようになります。以前のように手動でMとGの関連付けを解除したり、lockedgのチェックを各所で行ったりする必要がなくなるため、GC関連のコードがより簡潔で堅牢になります。

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

変更はすべて src/pkg/runtime/proc.c ファイル内で行われています。

  1. schedule 関数:
    • g->m->lockedg のチェックと、それに応じた stoplockedm() および execute(g->m->lockedg) の呼び出しが追加されました。
  2. dropg 関数:
    • 新しい関数 void dropg(void) が追加されました。この関数は、Mがロックされていない場合に、Mと現在のゴルーチン(g->m->curg)の関連付けを解除します。
  3. park0 関数:
    • gp->m = nil; g->m->curg = nil; の行が dropg(); に置き換えられました。
    • 以前存在した g->m->lockedg のチェックと関連するロジックが削除されました(これは schedule 関数に移動しました)。
  4. runtime·gosched0 関数:
    • gp->m = nil; g->m->curg = nil; の行が dropg(); に置き換えられました。
    • 以前存在した g->m->lockedg のチェックと関連するロジックが削除されました。
  5. goexit0 関数:
    • g->m->lockedg = nil; の行が追加されました。
    • g->m->curg = nil; g->m->lockedg = nil; の行が dropg(); に置き換えられました。
  6. exitsyscall0 関数:
    • gp->m = nil; g->m->curg = nil; の行が dropg(); に置き換えられました。

コアとなるコードの解説

schedule 関数の変更

 // src/pkg/runtime/proc.c
@@ -1320,6 +1320,11 @@ schedule(void)
  if(g->m->locks)
  runtime·throw("schedule: holding locks");
 
+if(g->m->lockedg) {
+    stoplockedm();
+    execute(g->m->lockedg);  // Never returns.
+}
+
 top:
  if(runtime·sched.gcwaiting) {
  gcstopm();

この変更は、schedule関数の冒頭に、現在のMが特定のゴルーチン(lockedg)にロックされているかどうかのチェックを追加しています。もしロックされている場合、scheduleは通常のスケジューリングロジックに進まず、stoplockedm()を呼び出してMを停止させ(必要であれば)、その後execute(g->m->lockedg)を呼び出して、ロックされているゴルーチンを再開します。executeはゴルーチンを実行し、そのゴルーチンが終了するか、別のゴルーチンに切り替わるまで戻らないため、このパスではschedule関数は実質的に終了します。これにより、lockedgのMが誤って他のゴルーチンをスケジューリングしてしまうことを防ぎ、lockedgの処理を一元化しています。

dropg 関数の追加

 // src/pkg/runtime/proc.c
@@ -1360,6 +1365,22 @@ top:
  execute(gp);
 }
 
+// dropg removes the association between m and the current goroutine m->curg (gp for short).
+// Typically a caller sets gp's status away from Grunning and then
+// immediately calls dropg to finish the job. The caller is also responsible
+// for arranging that gp will be restarted using runtime·ready at an
+// appropriate time. After calling dropg and arranging for gp to be
+// readied later, the caller can do other work but eventually should
+// call schedule to restart the scheduling of goroutines on this m.
+void
+dropg(void)
+{
+    if(g->m->lockedg == nil) {
+        g->m->curg->m = nil;
+        g->m->curg = nil;
+    }
+}
+
 // Puts the current goroutine into a waiting state and calls unlockf.
 // If unlockf returns false, the goroutine is resumed.
 void

dropg関数は、Mと現在のゴルーチン(g->m->curg)の関連付けを解除するための新しいヘルパー関数です。この関数は、Mがlockedgでない場合にのみ、g->m->curg->m = nil;(ゴルーチンからMへの参照を解除)とg->m->curg = nil;(Mからゴルーチンへの参照を解除)を実行します。これにより、Mは現在のゴルーチンから「解放」され、他の作業(例:GC)を行う準備ができます。この関数は、Mが特定のゴルーチンにロックされている場合は何もしません。

park0, runtime·gosched0, goexit0, exitsyscall0 の変更

これらの関数は、ゴルーチンが待機状態になったり、実行を譲ったり、終了したり、システムコールから戻ったりする際に、MとGの関連付けを解除する必要がある場所です。以前は、これらの関数内でMとGの関連付け解除ロジックが直接記述されていましたが、このコミットにより、そのロジックがdropg()の呼び出しに置き換えられました。

例: park0 関数の変更

 // src/pkg/runtime/proc.c
@@ -1396,8 +1417,8 @@ park0(G *gp)
  bool ok;
 
  gp->status = Gwaiting;
- gp->m = nil;
- g->m->curg = nil;
+ dropg();
+
  if(g->m->waitunlockf) {
  ok = g->m->waitunlockf(gp, g->m->waitlock);
  g->m->waitunlockf = nil;
@@ -1407,10 +1428,7 @@ park0(G *gp)
  execute(gp);  // Schedule it back, never returns.
  }
  }
- if(g->m->lockedg) {
- stoplockedm();
- execute(gp);  // Never returns.
- }
+
  schedule();
 }

park0関数では、gp->m = nil; g->m->curg = nil; の代わりに dropg(); が呼び出されています。また、以前存在したg->m->lockedgのチェックとexecute(gp)の呼び出しが削除されています。これは、lockedgの処理がschedule関数に一元化されたためです。他の関数でも同様の置き換えと削除が行われています。

goexit0関数では、dropg()の呼び出しの前にg->m->lockedg = nil;が追加されています。これは、goexit0がゴルーチンの終了処理を行う際に、もしそのゴルーチンがMにロックされていた場合、そのロックを明示的に解除する必要があるためです。dropg関数自体はlockedgnilでないとMとGの関連付けを解除しないため、この明示的な解除が必要になります。

これらの変更により、MとGの関連付け解除とlockedgの処理がよりモジュール化され、ランタイムコード全体の堅牢性と可読性が向上しています。

関連リンク

  • Go言語の公式ドキュメント: https://go.dev/
  • Goランタイムスケジューラに関するブログ記事やドキュメント(Goのバージョンによって詳細が異なる場合がありますが、基本的な概念は共通です)

参考にした情報源リンク

  • Goのソースコード (src/pkg/runtime/proc.c)
  • コミットメッセージと変更差分
  • Goランタイムスケジューラに関する一般的な知識(M, P, Gモデル、ゴルーチンの状態、lockedgmcallなど)
  • Goのガベージコレクションに関する情報
  • GoのIssue Tracker (Go CL 113110043): https://go.dev/cl/113110043
  • Goのコミット履歴 (GitHub): https://github.com/golang/go/commits/master
  • Goのスケジューラに関する解説記事 (例: "Go's work-stealing scheduler" by Daniel Martí, "Go scheduler: M, P, G" by Kavya Joshi) - 特定のURLは時間の経過とともに変わる可能性があるため、一般的な検索キーワードとして記載。

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

このコミットは、GoランタイムのスケジューラにおけるゴルーチンとM(OSスレッド)間の関連付けの管理方法をリファクタリングするものです。具体的には、dropgという新しいヘルパー関数を導入し、schedule関数がロックされたゴルーチン(lockedg)の処理をより適切に行うように変更することで、コードの堅牢性と保守性を向上させています。

コミット

commit 64c2083ebc4a071f842368154c77601e9463f5d5
Author: Russ Cox <rsc@golang.org>
Date:   Mon Jul 14 20:56:37 2014 -0400

    runtime: refactor routines for stopping, running goroutine from m
    
    This CL adds 'dropg', which is called to drop the association
    between m and its current goroutine, and it makes schedule
    handle locked goroutines correctly, instead of requiring all
    callers of schedule to do that.
    
    The effect is that if you want to take over an m for, say,
    garbage collection work while still allowing the current g
    to run on some other m, you can do an mcall to a function
    that is:
    
            // dissociate gp
            dropg();
            gp->status = Gwaiting; // for ready
    
            // put gp on run queue for others to find
            runtime·ready(gp);\
    
            /* ... do other work here ... */
    
            // done with m, let it run goroutines again
            schedule();
    
    Before this CL, the dropg() body had to be written explicitly,
    and the check for lockedg before schedule had to be
    written explicitly too, both of which make the code a bit
    more fragile than it needs to be.
    
    LGTM=iant
    R=dvyukov, iant
    CC=golang-codereviews, rlh
    https://golang.org/cl/113110043

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

https://github.com/golang/go/commit/64c2083ebc4a071f842368154c77601e9463f5d5

元コミット内容

このコミットは、Goランタイムにおいて、M(OSスレッド)から現在のG(ゴルーチン)を切り離すルーチンをリファクタリングするものです。具体的には、dropgという新しい関数を追加し、Mと現在のゴルーチンとの関連付けを解除できるようにします。また、schedule関数がロックされたゴルーチン(lockedg)を正しく処理するように変更し、scheduleの呼び出し元がその処理を行う必要がなくなりました。

これにより、例えばガベージコレクション(GC)作業のためにMを一時的に占有しつつ、現在のゴルーチンを別のMで実行させたい場合などに、より簡潔で堅牢なコードを書くことが可能になります。以前は、dropg()の本体やscheduleを呼び出す前のlockedgのチェックを明示的に記述する必要があり、コードが脆弱になる原因となっていました。

変更の背景

Goランタイムのスケジューラは、M(OSスレッド)、P(論理プロセッサ)、G(ゴルーチン)という3つの主要なエンティティを管理しています。MはOSスレッドを表し、GはGoの並行処理の単位であるゴルーチンを表します。PはMがGを実行するために必要なリソース(コンテキスト)を提供します。通常、MはPを介してGを実行します。

しかし、特定のシナリオ、特にランタイム内部の低レベルな操作(例:ガベージコレクション、Cgo呼び出し、runtime.LockOSThreadによるOSスレッドのロック)では、Mが一時的に特定のゴルーチンから切り離されたり、あるいは特定のゴルーチンにロックされたりする状況が発生します。

このコミット以前は、MとGの関連付けを解除するロジックや、Mが特定のゴルーチンにロックされている(lockedg)場合の処理が、ランタイム内の複数の場所で重複して記述されており、コードの重複と脆弱性を招いていました。特に、schedule関数を呼び出す前にlockedgのチェックを各呼び出し元で行う必要があり、これはエラーの温床となる可能性がありました。

このリファクタリングの目的は、これらの共通のロジックを抽象化し、一元化することで、ランタイムコードの保守性を高め、バグのリスクを減らすことにありました。特に、GCのような重要なランタイム操作が、ゴルーチンのスケジューリングに与える影響をより安全かつ効率的に管理できるようにすることが狙いです。

前提知識の解説

このコミットを理解するためには、Goランタイムのスケジューラとゴルーチンのライフサイクルに関する基本的な知識が必要です。

Goスケジューラ (M, P, G)

  • G (Goroutine): Goにおける並行実行の単位です。軽量なスレッドのようなもので、Goプログラムの関数呼び出しとして実行されます。Goランタイムによってスケジューリングされます。
  • M (Machine): OSスレッドを表します。Goランタイムは、OSスレッドをMとして抽象化し、その上でゴルーチンを実行します。MはOSのスケジューラによって管理されます。
  • P (Processor): 論理プロセッサを表します。MがGを実行するために必要なコンテキスト(ローカルな実行キュー、スケジューラの状態など)を提供します。Pの数は通常、GOMAXPROCS環境変数によって制御され、CPUコア数に等しいことが多いです。MはPを取得して初めてGを実行できます。

Goスケジューラは、M-P-Gモデルに基づいて動作します。MはPを取得し、PのローカルキューからGを取り出して実行します。Gがシステムコールなどでブロックされると、MはPを解放し、別のMがそのPを取得して他のGを実行できます。

ゴルーチンの状態

ゴルーチンは、そのライフサイクルにおいて様々な状態を取ります。このコミットに関連する主な状態は以下の通りです。

  • Grunning: ゴルーチンがM上で実行中である状態。
  • Grunnable: ゴルーチンが実行可能であり、PのローカルキューまたはグローバルキューでMにピックアップされるのを待っている状態。
  • Gwaiting: ゴルーチンが何らかのイベント(例:ネットワークI/O、タイマー、チャネル操作)を待っている状態。この状態のゴルーチンは実行可能ではありません。

lockedg (ロックされたゴルーチン)

Goランタイムには、特定のMにゴルーチンを「ロック」する機能があります。これは主に、Cgo呼び出し(C言語のコードを呼び出す場合)や、runtime.LockOSThread()関数を使用して、現在のゴルーチンが特定のOSスレッド(M)上で排他的に実行されることを保証する必要がある場合に用いられます。lockedgは、Mが現在ロックされているゴルーチンを指すポインタです。lockedgが設定されているMは、そのlockedg以外のゴルーチンを実行してはなりません。

mcall

mcallは、Goランタイムの非常に低レベルな関数で、現在のゴルーチンのスタックからMのシステムスタックに切り替えるために使用されます。これは、ゴルーチンのコンテキストから離れて、ランタイムの内部処理(例:スケジューラの操作、GC)を行う必要がある場合に用いられます。mcallが呼び出されると、現在のゴルーチンは一時的に中断され、Mはゴルーチンとは独立した処理を実行できるようになります。

ガベージコレクション (GC) とスケジューラ

Goのガベージコレクタは、メモリ管理の重要な部分です。GoのGCは、初期のバージョンでは「Stop-the-World (STW)」フェーズを持っていました。STWフェーズでは、すべてのゴルーチンの実行が一時的に停止され、GCが安全にメモリをスキャンし、マーク&スイープなどの処理を行います。このSTWフェーズ中、ランタイムのMはGC作業に専念する必要があり、通常のゴルーチン実行から切り離される必要があります。このコミットで導入されるdropgのようなメカニズムは、このようなSTWフェーズやその他のランタイム内部処理において、MとGの関連付けを一時的に解除するために利用されます。

技術的詳細

このコミットの技術的な核心は、GoランタイムのスケジューラにおけるMとGの関連付けの管理を、より抽象的で堅牢な方法に移行した点にあります。

dropg 関数の導入

以前は、Mから現在のゴルーチン(g->m->curg)を切り離す必要がある場合、g->m->curg->m = nil;g->m->curg = nil; のようなコードを各所で明示的に記述する必要がありました。このコミットでは、この共通のロジックをdropgという新しいヘルパー関数にカプセル化しました。

dropgの定義は以下の通りです。

void
dropg(void)
{
    if(g->m->lockedg == nil) { // Mが特定のゴルーチンにロックされていない場合のみ
        g->m->curg->m = nil;   // 現在のゴルーチンからMへの参照を解除
        g->m->curg = nil;      // Mから現在のゴルーチンへの参照を解除
    }
}

この関数は、Mがlockedg(特定のゴルーチンにロックされている状態)でない場合にのみ、Mと現在のゴルーチン(g->m->curg)の関連付けを解除します。lockedgである場合は、Mはそのゴルーチンに専念しているため、関連付けを解除すべきではありません。

このdropgの導入により、park0runtime·gosched0goexit0exitsyscall0といった複数の関数で重複していたMとGの関連付け解除ロジックがdropg()の呼び出しに置き換えられ、コードの簡潔性と保守性が向上しました。

schedule 関数における lockedg の一元的な処理

このコミットのもう一つの重要な変更点は、schedule関数がlockedgをより適切に処理するようにリファクタリングされたことです。

以前は、scheduleを呼び出す前に、呼び出し元がMがlockedgであるかどうかをチェックし、もしそうであれば、scheduleを呼び出さずにlockedgを再実行するロジックを明示的に記述する必要がありました。これは、schedulelockedgを考慮せずに他のゴルーチンをスケジューリングしてしまうことを防ぐためです。

このコミットでは、schedule関数の冒頭に以下のチェックが追加されました。

if(g->m->lockedg) {
    stoplockedm();           // ロックされたMを停止(必要であれば)
    execute(g->m->lockedg);  // ロックされたゴルーチンを実行(決して戻らない)
}

これにより、scheduleが呼び出された際に、Mがlockedgである場合は、scheduleは他のゴルーチンを探索する代わりに、直ちにそのlockedgを再実行するようになりました。この変更により、scheduleの呼び出し元はlockedgのチェックを行う必要がなくなり、コードの重複が解消され、scheduleの利用がより安全になりました。

ガベージコレクションシナリオへの応用

コミットメッセージで言及されているように、このリファクタリングはガベージコレクションのようなシナリオで特に有用です。GCがSTWフェーズに入る際、GCワーカーはMを一時的に占有してGC作業を行います。このとき、Mが以前実行していたゴルーチンは、GC作業が完了するまで中断されるか、あるいは別のMに移行される必要があります。

新しいdropgscheduleの動作により、GCワーカーは以下のような手順でMを占有し、ゴルーチンを切り離すことができます。

  1. mcallを使用して、GC作業を行う関数にMのコンテキストを切り替えます。
  2. GC作業関数内でdropg()を呼び出し、Mと以前のゴルーチン(gp)の関連付けを解除します。
  3. gp->status = Gwaiting; のようにゴルーチンの状態を更新し、runtime·ready(gp); を呼び出して、gpを他のMが実行できるように実行キューに戻します。
  4. GC作業を実行します。
  5. GC作業が完了したら、schedule()を呼び出して、Mが再び通常のゴルーチンをスケジューリングできるようにします。

この流れにより、GC作業中にMがゴルーチンから切り離され、GC作業が完了した後にMがスムーズにスケジューリングに戻れるようになります。以前のように手動でMとGの関連付けを解除したり、lockedgのチェックを各所で行ったりする必要がなくなるため、GC関連のコードがより簡潔で堅牢になります。

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

変更はすべて src/pkg/runtime/proc.c ファイル内で行われています。

  1. schedule 関数:
    • g->m->lockedg のチェックと、それに応じた stoplockedm() および execute(g->m->lockedg) の呼び出しが追加されました。
  2. dropg 関数:
    • 新しい関数 void dropg(void) が追加されました。この関数は、Mがロックされていない場合に、Mと現在のゴルーチン(g->m->curg)の関連付けを解除します。
  3. park0 関数:
    • gp->m = nil; g->m->curg = nil; の行が dropg(); に置き換えられました。
    • 以前存在した g->m->lockedg のチェックと関連するロジックが削除されました(これは schedule 関数に移動しました)。
  4. runtime·gosched0 関数:
    • gp->m = nil; g->m->curg = nil; の行が dropg(); に置き換えられました。
    • 以前存在した g->m->lockedg のチェックと関連するロジックが削除されました。
  5. goexit0 関数:
    • g->m->lockedg = nil; の行が追加されました。
    • g->m->curg = nil; g->m->lockedg = nil; の行が dropg(); に置き換えられました。
  6. exitsyscall0 関数:
    • gp->m = nil; g->m->curg = nil; の行が dropg(); に置き換えられました。

コアとなるコードの解説

schedule 関数の変更

 // src/pkg/runtime/proc.c
@@ -1320,6 +1320,11 @@ schedule(void)
  if(g->m->locks)
  runtime·throw("schedule: holding locks");
 
+if(g->m->lockedg) {
+    stoplockedm();
+    execute(g->m->lockedg);  // Never returns.
+}
+
 top:
  if(runtime·sched.gcwaiting) {
  gcstopm();

この変更は、schedule関数の冒頭に、現在のMが特定のゴルーチン(lockedg)にロックされているかどうかのチェックを追加しています。もしロックされている場合、scheduleは通常のスケジューリングロジックに進まず、stoplockedm()を呼び出してMを停止させ(必要であれば)、その後execute(g->m->lockedg)を呼び出して、ロックされているゴルーチンを再開します。executeはゴルーチンを実行し、そのゴルーチンが終了するか、別のゴルーチンに切り替わるまで戻らないため、このパスではschedule関数は実質的に終了します。これにより、lockedgのMが誤って他のゴルーチンをスケジューリングしてしまうことを防ぎ、lockedgの処理を一元化しています。

dropg 関数の追加

 // src/pkg/runtime/proc.c
@@ -1360,6 +1365,22 @@ top:
  execute(gp);
 }
 
+// dropg removes the association between m and the current goroutine m->curg (gp for short).
+// Typically a caller sets gp's status away from Grunning and then
+// immediately calls dropg to finish the job. The caller is also responsible
+// for arranging that gp will be restarted using runtime·ready at an
+// appropriate time. After calling dropg and arranging for gp to be
+// readied later, the caller can do other work but eventually should
+// call schedule to restart the scheduling of goroutines on this m.
+void
+dropg(void)
+{
+    if(g->m->lockedg == nil) {
+        g->m->curg->m = nil;
+        g->m->curg = nil;
+    }
+}
+
 // Puts the current goroutine into a waiting state and calls unlockf.
 // If unlockf returns false, the goroutine is resumed.
 void

dropg関数は、Mと現在のゴルーチン(g->m->curg)の関連付けを解除するための新しいヘルパー関数です。この関数は、Mがlockedgでない場合にのみ、g->m->curg->m = nil;(ゴルーチンからMへの参照を解除)とg->m->curg = nil;(Mからゴルーチンへの参照を解除)を実行します。これにより、Mは現在のゴルーチンから「解放」され、他の作業(例:GC)を行う準備ができます。この関数は、Mが特定のゴルーチンにロックされている場合は何もしません。

park0, runtime·gosched0, goexit0, exitsyscall0 の変更

これらの関数は、ゴルーチンが待機状態になったり、実行を譲ったり、終了したり、システムコールから戻ったりする際に、MとGの関連付けを解除する必要がある場所です。以前は、これらの関数内でMとGの関連付け解除ロジックが直接記述されていましたが、このコミットにより、そのロジックがdropg()の呼び出しに置き換えられました。

例: park0 関数の変更

 // src/pkg/runtime/proc.c
@@ -1396,8 +1417,8 @@ park0(G *gp)
  bool ok;
 
  gp->status = Gwaiting;
- gp->m = nil;
- g->m->curg = nil;
+ dropg();
+
  if(g->m->waitunlockf) {
  ok = g->m->waitunlockf(gp, g->m->waitlock);
  g->m->waitunlockf = nil;
@@ -1407,10 +1428,7 @@ park0(G *gp)
  execute(gp);  // Schedule it back, never returns.
  }
  }
- if(g->m->lockedg) {
- stoplockedm();
- execute(gp);  // Never returns.
- }
+
  schedule();
 }

park0関数では、gp->m = nil; g->m->curg = nil; の代わりに dropg(); が呼び出されています。また、以前存在したg->m->lockedgのチェックとexecute(gp)の呼び出しが削除されています。これは、lockedgの処理がschedule関数に一元化されたためです。他の関数でも同様の置き換えと削除が行われています。

goexit0関数では、dropg()の呼び出しの前にg->m->lockedg = nil;が追加されています。これは、goexit0がゴルーチンの終了処理を行う際に、もしそのゴルーチンがMにロックされていた場合、そのロックを明示的に解除する必要があるためです。dropg関数自体はlockedgnilでないとMとGの関連付けを解除しないため、この明示的な解除が必要になります。

これらの変更により、MとGの関連付け解除とlockedgの処理がよりモジュール化され、ランタイムコード全体の堅牢性と可読性が向上しています。

関連リンク

  • Go言語の公式ドキュメント: https://go.dev/
  • Goランタイムスケジューラに関するブログ記事やドキュメント(Goのバージョンによって詳細が異なる場合がありますが、基本的な概念は共通です)

参考にした情報源リンク

  • Goのソースコード (src/pkg/runtime/proc.c)
  • コミットメッセージと変更差分
  • Goランタイムスケジューラに関する一般的な知識(M, P, Gモデル、ゴルーチンの状態、lockedgmcallなど)
  • Goのガベージコレクションに関する情報
  • GoのIssue Tracker (Go CL 113110043): https://go.dev/cl/113110043
  • Goのコミット履歴 (GitHub): https://github.com/golang/go/commits/master
  • Goのスケジューラに関する解説記事 (例: "Go's work-stealing scheduler" by Daniel Martí, "Go scheduler: M, P, G" by Kavya Joshi) - 特定のURLは時間の経過とともに変わる可能性があるため、一般的な検索キーワードとして記載。