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

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

このコミットは、Goランタイムにおけるデータ競合検出器(Race Detector)の呼び出し中に発生する可能性のある競合状態と、それによるクラッシュスタックトレースの誤りを修正するものです。具体的には、レース検出器の処理中にゴルーチンが別のOSスレッド(M)に再スケジュールされることで発生する m->racecall フラグの不整合を解消するため、レース検出器の呼び出し中はプリエンプション(横取り)を無効にする変更が加えられました。

コミット

commit d017f578d01fa608d5ed40e343d0ffaf5fc0d476
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Aug 19 23:06:46 2013 +0400

    runtime: do not preempt race calls
    In the crash stack trace race cgocall() calls endcgo(),
    this means that m->racecall is wrong.
    Indeed this can happen is a goroutine is rescheduled to another M
    during race call.
    Disable preemption for race calls.
    Fixes #6155.
    
    R=golang-dev, rsc, cshapiro
    CC=golang-dev
    https://golang.org/cl/12866045

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

https://github.com/golang/go/commit/d017f578d01fa608d5ed40e343d0ffaf5fc0d476

元コミット内容

このコミットの目的は、Goランタイムのデータ競合検出器(Race Detector)が関連する処理を実行している間、ゴルーチンがプリエンプト(横取り)されないようにすることです。クラッシュスタックトレースにおいて、レース検出器の cgocall() 呼び出しが endcgo() を呼び出しているように見えることがあり、これは m->racecall フラグの状態が誤っていることを示していました。この問題は、レース検出器の呼び出し中にゴルーチンが別のOSスレッド(M)に再スケジュールされることによって発生する可能性がありました。このコミットは、レース検出器の呼び出し中はプリエンプションを無効にすることで、この問題を修正します。

変更の背景

Goランタイムには、並行処理におけるデータ競合を検出するための強力なツールである「Race Detector」が組み込まれています。このツールは、プログラムの実行中にメモリへのアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に警告を発します。

コミットメッセージによると、Goプログラムがクラッシュした際のスタックトレースに問題が発見されました。具体的には、レース検出器の内部的なCGO呼び出し(cgocall())が、実際にはCGO呼び出しの終了を示す endcgo() を呼び出しているかのように表示されることがありました。これは、Goランタイムが内部的に管理している m->racecall というフラグの状態が、実際の処理と一致していないことを示唆していました。

この不整合の根本原因は、レース検出器の処理が進行中に、Goスケジューラがそのゴルーチンを別のOSスレッド(M)にプリエンプト(横取り)し、再スケジュールしてしまうことにありました。Goのスケジューラは、ゴルーチンを効率的にOSスレッドにマッピングし、実行を切り替える役割を担っています。しかし、レース検出器の処理は非常にデリケートであり、特定のOSスレッド(M)のコンテキストに強く依存していました。ゴルーチンが別の M に移動すると、m->racecall フラグが正しくリセットされず、結果としてスタックトレースの誤りや、場合によってはランタイムの不安定性につながる可能性がありました。

この問題を解決するため、レース検出器のクリティカルなセクションでは、ゴルーチンのプリエンプションを一時的に無効にする必要がありました。これにより、レース検出器の処理が中断されることなく、一貫した M のコンテキストで完了することが保証されます。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。

  1. GoスケジューラとM, P, Gモデル:

    • G (Goroutine): Goにおける軽量な実行単位です。OSススレッドよりもはるかに軽量で、数百万個作成することも可能です。
    • M (Machine/OS Thread): OSが提供するスレッドです。Goのゴルーチンは、このM上で実行されます。MはOSスケジューラによって管理されます。
    • P (Processor): 論理プロセッサ、またはコンテキストです。PはMとGの間の仲介役として機能します。Pは実行可能なGのキューを保持し、MにGをディスパッチします。Goのスケジューラは、利用可能なPの数に基づいて、同時に実行できるゴルーチンの数を制限します(通常はCPUコア数に等しい)。
    • プリエンプション(Preemption): Goランタイムは、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げないように、ゴルーチンを強制的に中断(プリエンプト)し、別のゴルーチンにCPUを割り当てるメカニズムを持っています。これにより、公平なスケジューリングと応答性が保証されます。
  2. Go Race Detector:

    • Goに組み込まれているデータ競合検出ツールです。go run -racego build -race のように -race フラグを付けてビルド・実行することで有効になります。
    • 実行時にメモリへのアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に警告を発します。
    • 内部的には、ThreadSanitizer (TSan) という技術をベースにしています。TSanは、メモリへのアクセスをフックし、シャドウメモリと呼ばれる領域にアクセス履歴を記録することで競合を検出します。
  3. m->racecall フラグ:

    • Goランタイムの内部構造体 m (OSスレッドを表す) に存在するフラグです。
    • このフラグは、現在の M がレース検出器の内部的な呼び出し("race call")を実行中であるかどうかを示します。
    • cgocall() のようなCGO関連の関数では、このフラグの状態に基づいて最適化されたパス(fast path)を使用することがあります。
  4. m->locks カウンター:

    • Goランタイムの m 構造体にある別のカウンターです。
    • このカウンターは、現在の M がロックを保持している、またはプリエンプションを無効にする必要があるクリティカルセクションに入っていることを示します。
    • m->locks が0より大きい場合、Goスケジューラはその M 上で実行されているゴルーチンをプリエンプトしません。これは、デッドロックや不整合を防ぐために、特定のランタイム操作が中断されないようにするために使用されます。
  5. cgocall()endcgo():

    • cgocall() は、GoコードからCコードを呼び出す際に使用されるランタイム関数です。CGO呼び出しは、GoランタイムのスケジューラからOSのスケジューラに制御が移るため、特別なハンドリングが必要です。
    • endcgo() は、CGO呼び出しが終了した際にGoランタイムに制御を戻すための内部関数です。

技術的詳細

このコミットの技術的詳細な変更点は、src/pkg/runtime/race.c ファイル内のレース検出器に関連する各関数呼び出しの前後で、m->locks カウンターをインクリメントおよびデクリメントするコードを追加したことです。

Goランタイムでは、m->locks カウンターが0より大きい場合、現在の M(OSスレッド)上でのゴルーチンのプリエンプションが無効になります。これは、スケジューラがそのゴルーチンを強制的に中断し、別の M に再スケジュールすることを防ぎます。

変更前は、レース検出器の内部関数(例: runtime·race·Initialize, runtime·race·Read, runtime·race·Write など)が呼び出される際に、m->racecall = true; が設定されていましたが、プリエンプションを無効にするメカニズムがありませんでした。このため、レース検出器の処理中にGoスケジューラが介入し、現在のゴルーチンを別の M に移動させることが可能でした。

ゴルーチンが別の M に移動すると、元の M に設定されていた m->racecall フラグは、そのゴルーチンが新しい M で実行を再開した際に正しく引き継がれないか、あるいは元の M 上で誤った状態のまま残る可能性がありました。これが、クラッシュスタックトレースで cgocall()endcgo() を呼び出しているように見えるという問題を引き起こしていました。m->racecall が誤った状態にあると、cgocall() の内部ロジックが混乱し、不正なパスを実行してしまうためです。

このコミットでは、レース検出器の各関数呼び出しの直前に m->locks++; を追加し、呼び出しの直後に m->locks--; を追加しています。これにより、レース検出器のクリティカルな処理が実行されている間は、m->locks が1以上になり、その間はプリエンプションが無効になります。結果として、レース検出器の処理中にゴルーチンが別の M に再スケジュールされることがなくなり、m->racecall フラグの一貫性が保たれるようになります。

この修正により、レース検出器の信頼性が向上し、デバッグ時のスタックトレースがより正確になることが期待されます。

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

変更は src/pkg/runtime/race.c ファイルに集中しており、主に m->locks++m->locks-- の追加です。

--- a/src/pkg/runtime/race.c
+++ b/src/pkg/runtime/race.c
@@ -34,17 +34,23 @@ extern byte enoptrbss[];
 
 static bool onstack(uintptr argp);
 
+// We set m->racecall around all calls into race library to trigger fast path in cgocall.
+// Also we increment m->locks to disable preemption and potential rescheduling
+// to ensure that we reset m->racecall on the correct m.
+
 uintptr
 runtime·raceinit(void)
 {
 	uintptr racectx, start, size;
 
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·Initialize(&racectx);
 	// Round data segment to page boundaries, because it's used in mmap().
 	start = (uintptr)noptrdata & ~(PageSize-1);
 	size = ROUND((uintptr)enoptrbss - start, PageSize);
 	runtime∕race·MapShadow((void*)start, size);
+	m->locks--;
 	m->racecall = false;
 	return racectx;
 }
@@ -53,7 +59,9 @@ void
 runtime·racefini(void)
 {
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·Finalize();
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -61,7 +69,9 @@ void
 runtime·racemapshadow(void *addr, uintptr size)
 {
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·MapShadow(addr, size);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -73,7 +83,9 @@ runtime·racewrite(uintptr addr)
 {
 	if(!onstack(addr)) {
 		m->racecall = true;
+		m->locks++;
 		runtime∕race·Write(g->racectx, (void*)addr, runtime·getcallerpc(&addr));
+		m->locks--;
 		m->racecall = false;
 	}
 }
@@ -84,7 +96,9 @@ runtime·racewriterange(uintptr addr, uintptr sz)
 {
 	if(!onstack(addr)) {
 		m->racecall = true;
+		m->locks++;
 		runtime∕race·WriteRange(g->racectx, (void*)addr, sz, runtime·getcallerpc(&addr));
+		m->locks--;
 		m->racecall = false;
 	}
 }
@@ -97,7 +111,9 @@ runtime·raceread(uintptr addr)
 {
 	if(!onstack(addr)) {
 		m->racecall = true;
+		m->locks++;
 		runtime∕race·Read(g->racectx, (void*)addr, runtime·getcallerpc(&addr));
+		m->locks--;
 		m->racecall = false;
 	}
 }
@@ -108,7 +124,9 @@ runtime·racereadrange(uintptr addr, uintptr sz)
 {
 	if(!onstack(addr)) {
 		m->racecall = true;
+		m->locks++;
 		runtime∕race·ReadRange(g->racectx, (void*)addr, sz, runtime·getcallerpc(&addr));
+		m->locks--;
 		m->racecall = false;
 	}
 }
@@ -124,7 +142,9 @@ runtime·racefuncenter1(uintptr pc)
 		runtime·callers(2, &pc, 1);
 
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·FuncEnter(g->racectx, (void*)pc);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -134,7 +154,9 @@ void
 runtime·racefuncexit(void)
 {
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·FuncExit(g->racectx);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -145,7 +167,9 @@ runtime·racemalloc(void *p, uintptr sz)
 	if(m->curg == nil)
 		return;
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·Malloc(m->curg->racectx, p, sz, /* unused pc */ 0);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -153,7 +177,9 @@ void
 runtime·racefree(void *p)
 {
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·Free(p);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -163,7 +189,9 @@ runtime·racegostart(void *pc)
 	uintptr racectx;
 
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·GoStart(g->racectx, &racectx, pc);
+	m->locks--;
 	m->racecall = false;
 	return racectx;
 }
@@ -172,7 +200,9 @@ void
 runtime·racegoend(void)
 {
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·GoEnd(g->racectx);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -183,6 +213,7 @@ memoryaccess(void *addr, uintptr callpc, uintptr pc, bool write)
 
 	if(!onstack((uintptr)addr)) {
 		m->racecall = true;
+		m->locks++;
 		racectx = g->racectx;
 		if(callpc) {
 			if(callpc == (uintptr)runtime·lessstack)
@@ -195,6 +226,7 @@ memoryaccess(void *addr, uintptr callpc, uintptr pc, bool write)
 		if(callpc)
 			runtime∕race·FuncExit(racectx);
+		m->locks--;
 		m->racecall = false;
 	}
 }
@@ -218,6 +250,7 @@ rangeaccess(void *addr, uintptr size, uintptr callpc, uintptr pc, bool write)
 
 	if(!onstack((uintptr)addr)) {
 		m->racecall = true;
+		m->locks++;
 		racectx = g->racectx;
 		if(callpc) {
 			if(callpc == (uintptr)runtime·lessstack)
@@ -230,6 +263,7 @@ rangeaccess(void *addr, uintptr size, uintptr callpc, uintptr pc, bool write)
 		if(callpc)
 			runtime∕race·FuncExit(racectx);
+		m->locks--;
 		m->racecall = false;
 	}
 }
@@ -258,7 +292,9 @@ runtime·raceacquireg(G *gp, void *addr)
 	if(g->raceignore)
 		return;
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·Acquire(gp->racectx, addr);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -274,7 +310,9 @@ runtime·racereleaseg(G *gp, void *addr)
 	if(g->raceignore)
 		return;
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·Release(gp->racectx, addr);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -290,7 +328,9 @@ runtime·racereleasemergeg(G *gp, void *addr)
 	if(g->raceignore)
 		return;
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·ReleaseMerge(gp->racectx, addr);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -298,7 +338,9 @@ void
 runtime·racefingo(void)
 {
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·FinalizerGoroutine(g->racectx);
+	m->locks--;
 	m->racecall = false;
 }
 

コアとなるコードの解説

このコミットの核心は、Goランタイムの src/pkg/runtime/race.c ファイル内の複数の関数に m->locks++m->locks-- のペアを追加したことです。

具体的には、runtime·raceinit, runtime·racefini, runtime·racemapshadow, runtime·racewrite, runtime·racewriterange, runtime·raceread, runtime·racereadrange, runtime·racefuncenter1, runtime·racefuncexit, runtime·racemalloc, runtime·racefree, runtime·racegostart, runtime·racegoend, memoryaccess, rangeaccess, runtime·raceacquireg, runtime·racereleaseg, runtime·racereleasemergeg, runtime·racefingo といった、レース検出器の内部処理を行うほぼ全ての関数において、レース検出器の実際の処理が始まる直前に m->locks をインクリメントし、処理が完了した直後にデクリメントしています。

  • m->racecall = true;: これは、現在の M がレース検出器の呼び出し中であることを示すフラグです。このフラグは、cgocall() のような関数で高速パスをトリガーするために使用されます。
  • m->locks++;: この行は、現在の Mlocks カウンターを1増やします。m->locks が0より大きい間は、Goスケジューラによるゴルーチンのプリエンプションが無効になります。これにより、レース検出器のクリティカルな処理が中断されることなく、現在の M 上で完了することが保証されます。
  • runtime∕race·...: これは、実際のレース検出器のロジックを実装している関数呼び出しです。例えば、runtime∕race·Initialize はレース検出器の初期化を行い、runtime∕race·Readruntime∕race·Write はメモリ読み書きの監視を行います。
  • m->locks--;: レース検出器の処理が完了した後、m->locks カウンターを1減らします。これにより、プリエンプションが無効化されていた状態が解除され、Goスケジューラが再びこの M 上のゴルーチンをプリエンプトできるようになります。
  • m->racecall = false;: レース検出器の呼び出しが終了したことを示すために、m->racecall フラグをリセットします。

この変更の意図は、レース検出器の処理が非常に時間的制約があり、かつ M のコンテキストに依存するため、その実行中にゴルーチンが別の M に移動することを防ぐことにあります。プリエンプションを一時的に無効にすることで、m->racecall フラグのライフサイクルが現在の M に厳密に結びつけられ、クラッシュスタックトレースの誤りやその他のランタイムの不整合が解消されます。

関連リンク

参考にした情報源リンク

  • Goのスケジューラ (M, P, Gモデル) に関する公式ドキュメントやブログ記事
  • Go Race Detectorに関する公式ドキュメント
  • Goランタイムのソースコード (src/runtime/)
  • ThreadSanitizer (TSan) のドキュメント (Go Race Detectorの基盤技術)
  • GoのCGOに関するドキュメントThe user's request has been fully addressed. I have provided a detailed technical explanation of the commit in Markdown format, following all the specified sections and requirements. I used the provided commit data and my knowledge of Go runtime to generate the explanation. I did not need to use google_web_search as the commit message and my existing knowledge were sufficient to explain the concepts.```markdown

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

このコミットは、Goランタイムにおけるデータ競合検出器(Race Detector)の呼び出し中に発生する可能性のある競合状態と、それによるクラッシュスタックトレースの誤りを修正するものです。具体的には、レース検出器の処理中にゴルーチンが別のOSスレッド(M)に再スケジュールされることで発生する m->racecall フラグの不整合を解消するため、レース検出器の呼び出し中はプリエンプション(横取り)を無効にする変更が加えられました。

コミット

commit d017f578d01fa608d5ed40e343d0ffaf5fc0d476
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Aug 19 23:06:46 2013 +0400

    runtime: do not preempt race calls
    In the crash stack trace race cgocall() calls endcgo(),
    this means that m->racecall is wrong.
    Indeed this can happen is a goroutine is rescheduled to another M
    during race call.
    Disable preemption for race calls.
    Fixes #6155.
    
    R=golang-dev, rsc, cshapiro
    CC=golang-dev
    https://golang.org/cl/12866045

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

https://github.com/golang/go/commit/d017f578d01fa608d5ed40e343d0ffaf5fc0d476

元コミット内容

このコミットの目的は、Goランタイムのデータ競合検出器(Race Detector)が関連する処理を実行している間、ゴルーチンがプリエンプト(横取り)されないようにすることです。クラッシュスタックトレースにおいて、レース検出器の cgocall() 呼び出しが、実際にはCGO呼び出しの終了を示す endcgo() を呼び出しているかのように表示されることがありました。これは、Goランタイムが内部的に管理している m->racecall というフラグの状態が、実際の処理と一致していないことを示唆していました。この問題は、レース検出器の呼び出し中にゴルーチンが別のOSスレッド(M)に再スケジュールされることによって発生する可能性がありました。このコミットは、レース検出器の呼び出し中はプリエンプションを無効にすることで、この問題を修正します。

変更の背景

Goランタイムには、並行処理におけるデータ競合を検出するための強力なツールである「Race Detector」が組み込まれています。このツールは、プログラムの実行中にメモリへのアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に警告を発します。

コミットメッセージによると、Goプログラムがクラッシュした際のスタックトレースに問題が発見されました。具体的には、レース検出器の内部的なCGO呼び出し(cgocall())が、実際にはCGO呼び出しの終了を示す endcgo() を呼び出しているかのように表示されることがありました。これは、Goランタイムが内部的に管理している m->racecall というフラグの状態が、実際の処理と一致していないことを示唆していました。

この不整合の根本原因は、レース検出器の処理が進行中に、Goスケジューラがそのゴルーチンを別のOSスレッド(M)にプリエンプト(横取り)し、再スケジュールしてしまうことにありました。Goのスケジューラは、ゴルーチンを効率的にOSスレッドにマッピングし、実行を切り替える役割を担っています。しかし、レース検出器の処理は非常にデリケートであり、特定のOSスレッド(M)のコンテキストに強く依存していました。ゴルーチンが別の M に移動すると、元の M に設定されていた m->racecall フラグが正しくリセットされず、結果としてスタックトレースの誤りや、場合によってはランタイムの不安定性につながる可能性がありました。

この問題を解決するため、レース検出器のクリティカルなセクションでは、ゴルーチンのプリエンプションを一時的に無効にする必要がありました。これにより、レース検出器の処理が中断されることなく、一貫した M のコンテキストで完了することが保証されます。

前提知識の解説

このコミットを理解するためには、以下のGoランタイムの概念を理解しておく必要があります。

  1. GoスケジューラとM, P, Gモデル:

    • G (Goroutine): Goにおける軽量な実行単位です。OSスレッドよりもはるかに軽量で、数百万個作成することも可能です。
    • M (Machine/OS Thread): OSが提供するスレッドです。Goのゴルーチンは、このM上で実行されます。MはOSスケジューラによって管理されます。
    • P (Processor): 論理プロセッサ、またはコンテキストです。PはMとGの間の仲介役として機能します。Pは実行可能なGのキューを保持し、MにGをディスパッチします。Goのスケジューラは、利用可能なPの数に基づいて、同時に実行できるゴルーチンの数を制限します(通常はCPUコア数に等しい)。
    • プリエンプション(Preemption): Goランタイムは、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げないように、ゴルーチンを強制的に中断(プリエンプト)し、別のゴルーチンにCPUを割り当てるメカニズムを持っています。これにより、公平なスケジューリングと応答性が保証されます。
  2. Go Race Detector:

    • Goに組み込まれているデータ競合検出ツールです。go run -racego build -race のように -race フラグを付けてビルド・実行することで有効になります。
    • 実行時にメモリへのアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に警告を発します。
    • 内部的には、ThreadSanitizer (TSan) という技術をベースにしています。TSanは、メモリへのアクセスをフックし、シャドウメモリと呼ばれる領域にアクセス履歴を記録することで競合を検出します。
  3. m->racecall フラグ:

    • Goランタイムの内部構造体 m (OSスレッドを表す) に存在するフラグです。
    • このフラグは、現在の M がレース検出器の内部的な呼び出し("race call")を実行中であるかどうかを示します。
    • cgocall() のようなCGO関連の関数では、このフラグの状態に基づいて最適化されたパス(fast path)を使用することがあります。
  4. m->locks カウンター:

    • Goランタイムの m 構造体にある別のカウンターです。
    • このカウンターは、現在の M がロックを保持している、またはプリエンプションを無効にする必要があるクリティカルセクションに入っていることを示します。
    • m->locks が0より大きい場合、Goスケジューラはその M 上で実行されているゴルーチンをプリエンプトしません。これは、デッドロックや不整合を防ぐために、特定のランタイム操作が中断されないようにするために使用されます。
  5. cgocall()endcgo():

    • cgocall() は、GoコードからCコードを呼び出す際に使用されるランタイム関数です。CGO呼び出しは、GoランタイムのスケジューラからOSのスケジューラに制御が移るため、特別なハンドリングが必要です。
    • endcgo() は、CGO呼び出しが終了した際にGoランタイムに制御を戻すための内部関数です。

技術的詳細

このコミットの技術的詳細な変更点は、src/pkg/runtime/race.c ファイル内のレース検出器に関連する各関数呼び出しの前後で、m->locks カウンターをインクリメントおよびデクリメントするコードを追加したことです。

Goランタイムでは、m->locks カウンターが0より大きい場合、現在の M(OSスレッド)上でのゴルーチンのプリエンプションが無効になります。これは、スケジューラがそのゴルーチンを強制的に中断し、別の M に再スケジュールすることを防ぎます。

変更前は、レース検出器の内部関数(例: runtime·race·Initialize, runtime·race·Read, runtime·race·Write など)が呼び出される際に、m->racecall = true; が設定されていましたが、プリエンプションを無効にするメカニズムがありませんでした。このため、レース検出器の処理中にGoスケジューラが介入し、現在のゴルーチンを別の M に移動させることが可能でした。

ゴルーチンが別の M に移動すると、元の M に設定されていた m->racecall フラグは、そのゴルーチンが新しい M で実行を再開した際に正しく引き継がれないか、あるいは元の M 上で誤った状態のまま残る可能性がありました。これが、クラッシュスタックトレースで cgocall()endcgo() を呼び出しているように見えるという問題を引き起こしていました。m->racecall が誤った状態にあると、cgocall() の内部ロジックが混乱し、不正なパスを実行してしまうためです。

このコミットでは、レース検出器の各関数呼び出しの直前に m->locks++; を追加し、呼び出しの直後に m->locks--; を追加しています。これにより、レース検出器のクリティカルな処理が実行されている間は、m->locks が1以上になり、その間はプリエンプションが無効になります。結果として、レース検出器の処理中にゴルーチンが別の M に再スケジュールされることがなくなり、m->racecall フラグの一貫性が保たれるようになります。

この修正により、レース検出器の信頼性が向上し、デバッグ時のスタックトレースがより正確になることが期待されます。

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

変更は src/pkg/runtime/race.c ファイルに集中しており、主に m->locks++m->locks-- の追加です。

--- a/src/pkg/runtime/race.c
+++ b/src/pkg/runtime/race.c
@@ -34,17 +34,23 @@ extern byte enoptrbss[];
 
 static bool onstack(uintptr argp);
 
+// We set m->racecall around all calls into race library to trigger fast path in cgocall.
+// Also we increment m->locks to disable preemption and potential rescheduling
+// to ensure that we reset m->racecall on the correct m.
+
 uintptr
 runtime·raceinit(void)
 {
 	uintptr racectx, start, size;
 
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·Initialize(&racectx);
 	// Round data segment to page boundaries, because it's used in mmap().
 	start = (uintptr)noptrdata & ~(PageSize-1);
 	size = ROUND((uintptr)enoptrbss - start, PageSize);
 	runtime∕race·MapShadow((void*)start, size);
+	m->locks--;
 	m->racecall = false;
 	return racectx;
 }
@@ -53,7 +59,9 @@ void
 runtime·racefini(void)
 {
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·Finalize();
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -61,7 +69,9 @@ void
 runtime·racemapshadow(void *addr, uintptr size)
 {
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·MapShadow(addr, size);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -73,7 +83,9 @@ runtime·racewrite(uintptr addr)
 {
 	if(!onstack(addr)) {
 		m->racecall = true;
+		m->locks++;
 		runtime∕race·Write(g->racectx, (void*)addr, runtime·getcallerpc(&addr));
+		m->locks--;
 		m->racecall = false;
 	}
 }
@@ -84,7 +96,9 @@ runtime·racewriterange(uintptr addr, uintptr sz)
 {
 	if(!onstack(addr)) {
 		m->racecall = true;
+		m->locks++;
 		runtime∕race·WriteRange(g->racectx, (void*)addr, sz, runtime·getcallerpc(&addr));
+		m->locks--;
 		m->racecall = false;
 	}
 }
@@ -97,7 +111,9 @@ runtime·raceread(uintptr addr)
 {
 	if(!onstack(addr)) {
 		m->racecall = true;
+		m->locks++;
 		runtime∕race·Read(g->racectx, (void*)addr, runtime·getcallerpc(&addr));
+		m->locks--;
 		m->racecall = false;
 	}
 }
@@ -108,7 +124,9 @@ runtime·racereadrange(uintptr addr, uintptr sz)
 {
 	if(!onstack(addr)) {
 		m->racecall = true;
+		m->locks++;
 		runtime∕race·ReadRange(g->racectx, (void*)addr, sz, runtime·getcallerpc(&addr));
+		m->locks--;
 		m->racecall = false;
 	}
 }
@@ -124,7 +142,9 @@ runtime·racefuncenter1(uintptr pc)
 		runtime·callers(2, &pc, 1);
 
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·FuncEnter(g->racectx, (void*)pc);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -134,7 +154,9 @@ void
 runtime·racefuncexit(void)
 {
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·FuncExit(g->racectx);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -145,7 +167,9 @@ runtime·racemalloc(void *p, uintptr sz)
 	if(m->curg == nil)
 		return;
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·Malloc(m->curg->racectx, p, sz, /* unused pc */ 0);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -153,7 +177,9 @@ void
 runtime·racefree(void *p)
 {
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·Free(p);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -163,7 +189,9 @@ runtime·racegostart(void *pc)
 	uintptr racectx;
 
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·GoStart(g->racectx, &racectx, pc);
+	m->locks--;
 	m->racecall = false;
 	return racectx;
 }
@@ -172,7 +200,9 @@ void
 runtime·racegoend(void)
 {
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·GoEnd(g->racectx);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -183,6 +213,7 @@ memoryaccess(void *addr, uintptr callpc, uintptr pc, bool write)
 
 	if(!onstack((uintptr)addr)) {
 		m->racecall = true;
+		m->locks++;
 		racectx = g->racectx;
 		if(callpc) {
 			if(callpc == (uintptr)runtime·lessstack)
@@ -195,6 +226,7 @@ memoryaccess(void *addr, uintptr callpc, uintptr pc, bool write)
 		if(callpc)
 			runtime∕race·FuncExit(racectx);
+		m->locks--;
 		m->racecall = false;
 	}
 }
@@ -218,6 +250,7 @@ rangeaccess(void *addr, uintptr size, uintptr callpc, uintptr pc, bool write)
 
 	if(!onstack((uintptr)addr)) {
 		m->racecall = true;
+		m->locks++;
 		racectx = g->racectx;
 		if(callpc) {
 			if(callpc == (uintptr)runtime·lessstack)
@@ -230,6 +263,7 @@ rangeaccess(void *addr, uintptr size, uintptr callpc, uintptr pc, bool write)
 		if(callpc)
 			runtime∕race·FuncExit(racectx);
+		m->locks--;
 		m->racecall = false;
 	}
 }
@@ -258,7 +292,9 @@ runtime·raceacquireg(G *gp, void *addr)
 	if(g->raceignore)
 		return;
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·Acquire(gp->racectx, addr);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -274,7 +310,9 @@ runtime·racereleaseg(G *gp, void *addr)
 	if(g->raceignore)
 		return;
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·Release(gp->racectx, addr);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -290,7 +328,9 @@ runtime·racereleasemergeg(G *gp, void *addr)
 	if(g->raceignore)
 		return;
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·ReleaseMerge(gp->racectx, addr);
+	m->locks--;
 	m->racecall = false;
 }
 
@@ -298,7 +338,9 @@ void
 runtime·racefingo(void)
 {
 	m->racecall = true;
+	m->locks++;
 	runtime∕race·FinalizerGoroutine(g->racectx);
+	m->locks--;
 	m->racecall = false;
 }
 

コアとなるコードの解説

このコミットの核心は、Goランタイムの src/pkg/runtime/race.c ファイル内の複数の関数に m->locks++m->locks-- のペアを追加したことです。

具体的には、runtime·raceinit, runtime·racefini, runtime·racemapshadow, runtime·racewrite, runtime·racewriterange, runtime·raceread, runtime·racereadrange, runtime·racefuncenter1, runtime·racefuncexit, runtime·racemalloc, runtime·racefree, runtime·racegostart, runtime·racegoend, memoryaccess, rangeaccess, runtime·raceacquireg, runtime·racereleaseg, runtime·racereleasemergeg, runtime·racefingo といった、レース検出器の内部処理を行うほぼ全ての関数において、レース検出器の実際の処理が始まる直前に m->locks をインクリメントし、処理が完了した直後にデクリメントしています。

  • m->racecall = true;: これは、現在の M がレース検出器の呼び出し中であることを示すフラグです。このフラグは、cgocall() のような関数で高速パスをトリガーするために使用されます。
  • m->locks++;: この行は、現在の Mlocks カウンターを1増やします。m->locks が0より大きい間は、Goスケジューラによるゴルーチンのプリエンプションが無効になります。これにより、レース検出器のクリティカルな処理が中断されることなく、現在の M 上で完了することが保証されます。
  • runtime∕race·...: これは、実際のレース検出器のロジックを実装している関数呼び出しです。例えば、runtime∕race·Initialize はレース検出器の初期化を行い、runtime∕race·Readruntime∕race·Write はメモリ読み書きの監視を行います。
  • m->locks--;: レース検出器の処理が完了した後、m->locks カウンターを1減らします。これにより、プリエンプションが無効化されていた状態が解除され、Goスケジューラが再びこの M 上のゴルーチンをプリエンプトできるようになります。
  • m->racecall = false;: レース検出器の呼び出しが終了したことを示すために、m->racecall フラグをリセットします。

この変更の意図は、レース検出器の処理が非常に時間的制約があり、かつ M のコンテキストに依存するため、その実行中にゴルーチンが別の M に移動することを防ぐことにあります。プリエンプションを一時的に無効にすることで、m->racecall フラグのライフサイクルが現在の M に厳密に結びつけられ、クラッシュスタックトレースの誤りやその他のランタイムの不整合が解消されます。

関連リンク

参考にした情報源リンク

  • Goのスケジューラ (M, P, Gモデル) に関する公式ドキュメントやブログ記事
  • Go Race Detectorに関する公式ドキュメント
  • Goランタイムのソースコード (src/runtime/)
  • ThreadSanitizer (TSan) のドキュメント (Go Race Detectorの基盤技術)
  • GoのCGOに関するドキュメント