[インデックス 16840] ファイルの概要
このコミットは、Goランタイムのチャネル操作に関連するclosechan
関数の内部実装に対する変更です。具体的には、closechan
関数の本体内でスタック分割(stack split)を許可するように修正されています。これにより、runtime.lock
呼び出し中に、より多くのスタック領域が利用可能になります。
コミット
commit 10d1e55103c17f4b379729f2d6b40327cefea6be
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Mon Jul 22 20:47:39 2013 +0400
runtime: allow stack split in body of closechan
This gives more space during the call to runtime.lock.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/11679043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/10d1e55103c17f4b379729f2d6b40327cefea6be
元コミット内容
Goランタイムのclosechan
関数において、その本体内でスタック分割を許可する変更です。これにより、runtime.lock
関数が呼び出される際に、より多くのスタック空間が確保できるようになります。
変更の背景
Goランタイムは、ゴルーチン(goroutine)のスタックサイズを動的に管理します。ゴルーチンは最初は小さなスタックで開始し、必要に応じてスタックを拡張します。この拡張プロセスは「スタック分割(stack split)」と呼ばれ、関数呼び出しのプロローグで行われます。しかし、特定のランタイム関数、特にロック操作のようなクリティカルなセクションでは、スタック分割が意図的に抑制されることがあります。これは、スタック分割自体が追加のスタック空間を必要とし、またロックを保持している間にスタック分割が発生すると、デッドロックやパフォーマンスの問題を引き起こす可能性があるためです。
このコミットの背景には、closechan
関数内でruntime.lock
を呼び出す際に、スタック空間が不足する可能性があったことが考えられます。runtime.lock
はミューテックスをロックする操作であり、Goランタイムの非常に低レベルな部分で実行されます。このような低レベルの関数が十分なスタック空間を持たない場合、予期せぬクラッシュや不安定な動作につながる可能性があります。
コミットメッセージにある「This gives more space during the call to runtime.lock.」という記述から、runtime.lock
呼び出し時にスタックが不足するシナリオがあったことが示唆されます。closechan
関数が直接runtime.lock
を呼び出すのではなく、ヘルパー関数を導入し、そのヘルパー関数内でスタック分割を許可することで、runtime.lock
が呼び出されるコンテキストでより多くのスタック空間を確保できるようにしたと考えられます。
前提知識の解説
1. Goのゴルーチンとスタック管理
Goのゴルーチンは軽量なスレッドのようなもので、数千から数百万のゴルーチンを同時に実行できます。各ゴルーチンは独自のスタックを持ちますが、そのスタックは固定サイズではなく、必要に応じて動的に拡大・縮小します。この動的なスタック管理は、Goランタイムの重要な機能の一つです。
- スタック分割 (Stack Split): ゴルーチンが関数を呼び出す際、Goランタイムは現在のスタックがその関数を実行するのに十分な大きさがあるかをチェックします。もし不足している場合、ランタイムはより大きな新しいスタックを割り当て、古いスタックの内容を新しいスタックにコピーし、実行を新しいスタックに切り替えます。このプロセスがスタック分割です。
//go:noescape
と//go:nosplit
: Goコンパイラには、特定の関数に対してスタック分割を抑制するディレクティブがあります。//go:noescape
: 関数がヒープにポインタをエスケープしないことを示します。これにより、コンパイラはスタック割り当てを最適化できます。//go:nosplit
: 関数がスタック分割を行わないことを示します。これは、非常に短い関数や、ランタイムのクリティカルなセクション(例えば、ロックを保持している間)でスタック分割によるオーバーヘッドやデッドロックのリスクを避けたい場合に使用されます。
2. Goのチャネル (Channels)
チャネルは、ゴルーチン間で値を送受信するためのGoの同期プリミティブです。チャネルは、Goの並行処理モデルの中心的な要素であり、ゴルーチン間の安全な通信を可能にします。
- チャネルのクローズ (Closing Channels):
close(ch)
関数を使ってチャネルをクローズできます。クローズされたチャネルからは、それ以上値を送信することはできませんが、既に送信された値や、チャネルが空になった後にはゼロ値を受信できます。チャネルをクローズすることは、送信側がこれ以上値を送信しないことを受信側に通知するメカニズムとして機能します。
3. Goランタイムのロック (runtime.lock)
Goランタイムは、内部的なデータ構造(スケジューラ、メモリ管理、チャネルなど)の一貫性を保つために、ミューテックス(mutex)を使用します。runtime.lock
は、これらの内部ミューテックスをロックするための低レベルな関数です。ランタイムのロックは、ユーザーコードが直接呼び出すものではなく、Goランタイムの内部で並行処理の安全性を確保するために使用されます。
4. データ競合検出 (Race Detection)
Goには、並行処理におけるデータ競合(data race)を検出するための組み込みのツールがあります。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期されていない場合に発生します。runtime.racewritepc
のような関数は、このデータ競合検出器の一部として使用されます。
技術的詳細
このコミットの主要な変更点は、runtime·closechan
関数が直接チャネルをクローズするロジックを持つのではなく、static void closechan(Hchan *c, void *pc)
という新しいヘルパー関数を導入し、そのヘルパー関数に実際のクローズ処理を委譲するようにした点です。
元のruntime·closechan
関数とreflect·chanclose
関数は、単に新しいclosechan
ヘルパー関数を呼び出すだけになりました。
// closechan(sel *byte);
#pragma textflag 7
void
runtime·closechan(Hchan *c)
{
closechan(c, runtime·getcallerpc(&c));
}
// For reflect
// func chanclose(c chan)
#pragma textflag 7
void
reflect·chanclose(Hchan *c)
{
closechan(c, runtime·getcallerpc(&c));
}
static void
closechan(Hchan *c, void *pc)
{
// ... 実際のチャネルクローズロジック ...
}
ここで重要なのは、#pragma textflag 7
というディレクティブです。これはGoコンパイラに対する指示で、この関数がスタック分割を許可することを示します(Go 1.0の時代では、textflag 7
はNOSPLIT
ではないことを意味していました。つまり、スタック分割が許可される関数です)。
元のruntime·closechan
関数は、おそらく//go:nosplit
のようなディレクティブ、またはそれに相当する内部的なメカニズムによってスタック分割が抑制されていた可能性があります。これは、runtime·closechan
がチャネルのロックを取得する前に、非常に低レベルな操作を行うためかもしれません。しかし、チャネルのロックを取得した後、つまりruntime.lock(c)
が呼び出された後では、スタック分割が許可されることで、runtime.lock
がより多くのスタック空間を必要とする場合に、スタックオーバーフローを避けることができます。
新しいclosechan
ヘルパー関数は、runtime·closechan
やreflect·chanclose
から呼び出される際に、呼び出し元のPC(プログラムカウンタ)を引数として受け取ります。このPCは、データ競合検出のためのruntime·racewritepc
関数に渡されます。
if(raceenabled) {
runtime·racewritepc(c, pc, runtime·closechan);
runtime·racerelease(c);
}
以前はruntime·racewritepc(c, runtime·getcallerpc(&c), runtime·closechan);
のように、runtime·closechan
自身の呼び出し元PCを取得していましたが、ヘルパー関数に分離されたことで、元のruntime·closechan
またはreflect·chanclose
の呼び出し元PCを正確にracewritepc
に渡せるようになりました。これは、データ競合の報告において、より正確なコンテキストを提供するために重要です。
この変更により、closechan
の本体内でスタック分割が許可され、特にruntime.lock
が呼び出される際に、ゴルーチンがより大きなスタックを必要とする場合に、スタックを安全に拡張できるようになります。これにより、スタックオーバーフローによるクラッシュのリスクが軽減され、ランタイムの堅牢性が向上します。
コアとなるコードの変更箇所
変更はsrc/pkg/runtime/chan.c
ファイルに集中しています。
--- a/src/pkg/runtime/chan.c
+++ b/src/pkg/runtime/chan.c
@@ -1214,10 +1214,27 @@ reflect·rselect(Slice cases, intgo chosen, uintptr word, bool recvOK)
FLUSH(&recvOK);
}
+static void closechan(Hchan *c, void *pc);
+
// closechan(sel *byte);
#pragma textflag 7
void
runtime·closechan(Hchan *c)
+{
+ closechan(c, runtime·getcallerpc(&c));
+}
+
+// For reflect
+// func chanclose(c chan)
+#pragma textflag 7
+void
+reflect·chanclose(Hchan *c)
+{
+ closechan(c, runtime·getcallerpc(&c));
+}
+
+static void
+closechan(Hchan *c, void *pc)
{
SudoG *sg;
G* gp;
@@ -1235,7 +1252,7 @@ runtime·closechan(Hchan *c)
}
if(raceenabled) {
- runtime·racewritepc(c, runtime·getcallerpc(&c), runtime·closechan);
+ runtime·racewritepc(c, pc, runtime·closechan);
runtime·racerelease(c);
}
@@ -1264,14 +1281,6 @@ runtime·closechan(Hchan *c)
runtime·unlock(c);
}
-// For reflect
-// func chanclose(c chan)
-void
-reflect·chanclose(Hchan *c)
-{
- runtime·closechan(c);
-}
-
// For reflect
// func chanlen(c chan) (len int)\n
コアとなるコードの解説
-
新しいヘルパー関数の宣言と定義:
+static void closechan(Hchan *c, void *pc); // ... +static void +closechan(Hchan *c, void *pc) +{ // ... (元のruntime·closechanの本体ロジックがここに移された) ... }
closechan
という新しい静的ヘルパー関数が導入されました。この関数は、チャネルのポインタHchan *c
と、呼び出し元のプログラムカウンタvoid *pc
を引数に取ります。元のruntime·closechan
の実際のチャネルクローズロジックがこの関数に移されました。 -
runtime·closechan
の変更:void runtime·closechan(Hchan *c) { closechan(c, runtime·getcallerpc(&c)); }
runtime·closechan
は、もはやチャネルクローズのロジックを直接含まず、新しく定義されたclosechan
ヘルパー関数を呼び出すだけになりました。runtime·getcallerpc(&c)
は、runtime·closechan
の呼び出し元のプログラムカウンタを取得し、それをclosechan
ヘルパー関数に渡します。 -
reflect·chanclose
の変更:void reflect·chanclose(Hchan *c) { closechan(c, runtime·getcallerpc(&c)); }
reflect·chanclose
も同様に、closechan
ヘルパー関数を呼び出すように変更されました。これはリフレクションを通じてチャネルをクローズする際に使用される関数です。 -
データ競合検出ロジックの変更:
- runtime·racewritepc(c, runtime·getcallerpc(&c), runtime·closechan); + runtime·racewritepc(c, pc, runtime·closechan);
raceenabled
(データ競合検出が有効な場合)のブロック内で、runtime·racewritepc
に渡されるPCが、runtime·getcallerpc(&c)
から、closechan
ヘルパー関数に引数として渡されたpc
に変更されました。これにより、データ競合の報告が、runtime·closechan
またはreflect·chanclose
の実際の呼び出し元を指すようになり、より正確な情報が提供されます。
この変更の核心は、runtime·closechan
とreflect·chanclose
がスタック分割を抑制する可能性がある一方で、それらが呼び出すclosechan
ヘルパー関数はスタック分割が許可されるように設計されている点です。これにより、closechan
ヘルパー関数内でruntime.lock
のようなスタックを消費する可能性のある操作が行われる際に、必要に応じてスタックを拡張できるようになり、スタックオーバーフローのリスクが軽減されます。
関連リンク
- Go言語のチャネルに関する公式ドキュメント: https://go.dev/tour/concurrency/2
- Goのスタック管理に関する議論(古い情報も含むが概念は参考になる): https://go.dev/doc/articles/go_mem.html
- Goのランタイムソースコード(
src/runtime/chan.go
やsrc/runtime/proc.go
など)
参考にした情報源リンク
- Goのコミット履歴: https://github.com/golang/go/commits/master
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/
- Goのスタック分割に関する情報 (例:
//go:nosplit
): https://go.dev/src/runtime/asm_amd64.s (アセンブリコード内のコメントやディレクティブ) - Goのデータ競合検出器に関する情報: https://go.dev/blog/race-detector
- Goの
textflag
に関する情報 (Goのバージョンによって意味合いが変わる可能性があるので注意): https://go.dev/src/cmd/compile/internal/gc/lex.go (コンパイラのソースコード) - Goのチャネル実装に関する詳細な解説記事(非公式)
- Goのランタイムロックに関する情報(非公式)