[インデックス 14112] ファイルの概要
コミット
commit 27e93fbd00de218ba53a5b22333246abde88028c
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Oct 10 18:06:29 2012 +0400
runtime: fix race detector handling of stackalloc()
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6632051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/27e93fbd00de218ba53a5b22333246abde88028c
元コミット内容
runtime: fix race detector handling of stackalloc()
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6632051
変更の背景
このコミットは、Go言語のランタイムにおけるデータ競合検出器(Race Detector)が、stackalloc()
関数によって割り当てられたメモリを正しく扱えないというバグを修正するためのものです。
Goのランタイムは、プログラムの実行中に発生する可能性のあるデータ競合を検出するための強力なツールであるデータ競合検出器を内蔵しています。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生し、予測不能な動作やバグの原因となります。
stackalloc()
は、Goランタイム内部で使用される関数で、スタック上にメモリを割り当てるために利用されます。しかし、データ競合検出器がこのstackalloc()
によって割り当てられたメモリの操作を適切に監視できていなかったため、誤った競合検出結果(偽陽性または偽陰性)が生じる可能性がありました。特に、stackalloc()
がg0
(スケジューラやガベージコレクタなどのランタイム内部処理を実行する特別なゴルーチン)から呼び出される場合に問題が発生していました。g0
は通常のユーザーゴルーチンとは異なるコンテキストで動作するため、データ競合検出器が期待するg
(現在のゴルーチン)のコンテキストが利用できないことが原因と考えられます。
この問題は、データ競合検出器の信頼性を損ない、開発者が実際の競合と誤検出を区別することを困難にするため、修正が必要でした。
前提知識の解説
Goランタイム (Go Runtime)
Goランタイムは、Goプログラムの実行を管理するシステムです。これには、ゴルーチン(軽量スレッド)のスケジューリング、メモリ管理(ガベージコレクションを含む)、チャネル通信、およびその他の低レベルの操作が含まれます。ランタイムはGoプログラムとオペレーティングシステム間の橋渡し役となり、Goの並行処理モデルを可能にしています。
データ競合検出器 (Race Detector)
Goのデータ競合検出器は、Go 1.1で導入された強力な診断ツールです。これは、実行時にメモリへのアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合にデータ競合を報告します。データ競合は、並行プログラムにおける最も一般的なバグの原因の一つであり、検出が非常に困難です。データ競合検出器は、これらのバグを開発段階で特定し、修正するのに役立ちます。
データ競合検出器は、プログラムの実行をインストルメント化(計測コードを挿入)することで機能します。メモリへの読み書きが発生するたびに、検出器はアクセス履歴を記録し、競合のパターンをチェックします。
ゴルーチン (Goroutine)
ゴルーチンは、Go言語における並行実行の単位です。OSのスレッドよりもはるかに軽量で、数千から数百万のゴルーチンを同時に実行できます。ゴルーチンはGoランタイムによってスケジューリングされ、複数のOSスレッドに多重化されます。
g0
ゴルーチン
g0
は、Goランタイムが内部処理(スケジューリング、ガベージコレクション、システムコールなど)を実行するために使用する特別なゴルーチンです。通常のユーザーゴルーチンとは異なり、g0
はシステムスタックを使用し、ユーザーコードを実行することはありません。このコミットの文脈では、stackalloc()
がg0
から呼び出される可能性があることが問題の原因となっていました。
m
(Machine) と g
(Goroutine)
Goランタイムの内部では、m
(Machine)とg
(Goroutine)という概念が重要です。
m
はOSのスレッドを表します。Goランタイムは、複数のm
を使用してゴルーチンを実行します。g
はゴルーチンを表します。各g
は、実行コンテキスト(スタック、プログラムカウンタなど)を保持します。
m->curg
は、現在m
によって実行されているゴルーチンを指します。通常のユーザーコードではg
は現在のゴルーチンを指しますが、ランタイム内部ではm->curg
がより正確な現在のゴルーチンを示します。
stackalloc()
stackalloc()
は、Goランタイムの内部関数であり、スタック上にメモリを割り当てるために使用されます。これは、ヒープ割り当てよりも高速であり、一時的なデータや小さなオブジェクトのために利用されます。
runtime∕race·Malloc
これは、データ競合検出器の内部関数で、メモリ割り当てイベントを検出器に報告するために使用されます。この関数は、割り当てられたメモリのアドレス、サイズ、および割り当てを行ったゴルーチンのIDなどの情報を受け取ります。
技術的詳細
このコミットの核心は、データ競合検出器がstackalloc()
によって割り当てられたメモリを追跡する際に、正しいゴルーチンIDを使用するように修正することです。
元のコードでは、runtime·racemalloc
関数内でg->goid-1
を使用して現在のゴルーチンIDを取得していました。しかし、stackalloc()
がg0
から呼び出される場合、g
(現在のユーザーゴルーチン)はnil
であるか、期待されるゴルーチンではない可能性があります。g0
はユーザーゴルーチンではないため、g
が指すゴルーチンは存在しないか、関連性のないゴルーチンである可能性があります。
修正では、以下の2つの変更が導入されました。
m->curg
の使用:runtime·racemalloc
関数内で、ゴルーチンIDを取得するためにg->goid-1
の代わりにm->curg->goid-1
を使用するように変更されました。m->curg
は、現在m
(OSスレッド)によって実行されているゴルーチンを常に正確に指します。これにより、stackalloc()
がg0
から呼び出された場合でも、データ競合検出器は正しいコンテキスト(g0
または他のランタイムゴルーチン)でメモリ割り当てを追跡できるようになります。m->curg == nil
のチェック:runtime·racemalloc
関数の冒頭にif(m->curg == nil)
というチェックが追加されました。これは、runtime·stackalloc()
がg0
から呼び出される可能性があるためです。g0
は特別なゴルーチンであり、場合によってはm->curg
がnil
になることがあります。このようなケースでは、データ競合検出器による追跡は不要または不適切であるため、関数を早期に終了させることで、パニックや不正な動作を防ぎます。
これらの変更により、データ競合検出器はstackalloc()
によって割り当てられたメモリに対するアクセスを、それがどのゴルーチン(ユーザーゴルーチンかランタイム内部ゴルーチンかに関わらず)によって行われたかに関わらず、正確に監視できるようになりました。これにより、データ競合検出器の信頼性が向上し、偽陽性や偽陰性の報告が減少します。
コアとなるコードの変更箇所
src/pkg/runtime/race.c
ファイルの runtime·racemalloc
関数が変更されています。
--- a/src/pkg/runtime/race.c
+++ b/src/pkg/runtime/race.c
@@ -92,8 +92,11 @@ runtime·racefuncexit(void)
void
runtime·racemalloc(void *p, uintptr sz, void *pc)
{
+ // use m->curg because runtime·stackalloc() is called from g0
+ if(m->curg == nil)
+ return;
m->racecall = true;
- runtime∕race·Malloc(g->goid-1, p, sz, pc);\n
+ runtime∕race·Malloc(m->curg->goid-1, p, sz, pc);
m->racecall = false;
}
コアとなるコードの解説
変更されたruntime·racemalloc
関数は、メモリ割り当てが発生した際にデータ競合検出器にその情報を通知する役割を担っています。
-
if(m->curg == nil)
の追加: この行は、runtime·racemalloc
が呼び出された時点で、現在のm
(OSスレッド)に紐付けられたゴルーチン(m->curg
)が存在しない場合に、関数を即座に終了させます。コメントにもあるように、runtime·stackalloc()
はg0
(ランタイム内部ゴルーチン)から呼び出されることがあり、その際にm->curg
がnil
である状況が発生し得ます。このような場合、データ競合検出器による追跡は不要または不適切であるため、このチェックによって安全に処理をスキップします。 -
runtime∕race·Malloc(m->curg->goid-1, p, sz, pc);
への変更: 元のコードではg->goid-1
を使用していましたが、これは現在のユーザーゴルーチンg
のIDを期待していました。しかし、stackalloc()
がg0
のようなランタイム内部ゴルーチンから呼び出される場合、g
は正しいコンテキストを指さない可能性があります。 新しいコードでは、m->curg->goid-1
を使用しています。m->curg
は、現在m
(OSスレッド)によって実行されているゴルーチンを常に正確に指します。これにより、stackalloc()
がどのゴルーチン(ユーザーゴルーチン、g0
、またはその他のランタイムゴルーチン)によって呼び出されたかに関わらず、データ競合検出器は正しいゴルーチンIDをruntime∕race·Malloc
に渡し、メモリ割り当てイベントを正確に記録できるようになります。
これらの変更により、データ競合検出器はstackalloc()
によって割り当てられたメモリに対するアクセスを、より堅牢かつ正確に追跡できるようになり、データ競合の検出精度が向上しました。
関連リンク
- Go Race Detector: https://go.dev/doc/articles/race_detector
- Go Runtime Source Code (race.c): https://github.com/golang/go/blob/master/src/runtime/race.go (Go 1.1以降はrace.goに移行している可能性がありますが、当時のCコードの文脈です)
- Go CL 6632051: https://golang.org/cl/6632051
参考にした情報源リンク
- Go Race Detector Documentation
- Go Runtime Source Code
- Go Mailing Lists and Issue Trackers (特にCL 6632051に関連する議論)
- Go言語の内部構造に関する一般的な情報源
- データ競合に関する一般的なプログラミングの概念