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

[インデックス 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 7NOSPLITではないことを意味していました。つまり、スタック分割が許可される関数です)。

元のruntime·closechan関数は、おそらく//go:nosplitのようなディレクティブ、またはそれに相当する内部的なメカニズムによってスタック分割が抑制されていた可能性があります。これは、runtime·closechanがチャネルのロックを取得する前に、非常に低レベルな操作を行うためかもしれません。しかし、チャネルのロックを取得した後、つまりruntime.lock(c)が呼び出された後では、スタック分割が許可されることで、runtime.lockがより多くのスタック空間を必要とする場合に、スタックオーバーフローを避けることができます。

新しいclosechanヘルパー関数は、runtime·closechanreflect·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

コアとなるコードの解説

  1. 新しいヘルパー関数の宣言と定義:

    +static void closechan(Hchan *c, void *pc);
    // ...
    +static void
    +closechan(Hchan *c, void *pc)
    +{
    // ... (元のruntime·closechanの本体ロジックがここに移された) ...
    }
    

    closechanという新しい静的ヘルパー関数が導入されました。この関数は、チャネルのポインタHchan *cと、呼び出し元のプログラムカウンタvoid *pcを引数に取ります。元のruntime·closechanの実際のチャネルクローズロジックがこの関数に移されました。

  2. runtime·closechanの変更:

    void
    runtime·closechan(Hchan *c)
    {
    	closechan(c, runtime·getcallerpc(&c));
    }
    

    runtime·closechanは、もはやチャネルクローズのロジックを直接含まず、新しく定義されたclosechanヘルパー関数を呼び出すだけになりました。runtime·getcallerpc(&c)は、runtime·closechanの呼び出し元のプログラムカウンタを取得し、それをclosechanヘルパー関数に渡します。

  3. reflect·chancloseの変更:

    void
    reflect·chanclose(Hchan *c)
    {
    	closechan(c, runtime·getcallerpc(&c));
    }
    

    reflect·chancloseも同様に、closechanヘルパー関数を呼び出すように変更されました。これはリフレクションを通じてチャネルをクローズする際に使用される関数です。

  4. データ競合検出ロジックの変更:

    -		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·closechanreflect·chancloseがスタック分割を抑制する可能性がある一方で、それらが呼び出すclosechanヘルパー関数はスタック分割が許可されるように設計されている点です。これにより、closechanヘルパー関数内でruntime.lockのようなスタックを消費する可能性のある操作が行われる際に、必要に応じてスタックを拡張できるようになり、スタックオーバーフローのリスクが軽減されます。

関連リンク

参考にした情報源リンク