[インデックス 18134] ファイルの概要
このコミットは、Goランタイムにおけるマップのキーと値に対するデータ競合検出の挙動を改善するものです。特に、参照渡しされるようになったマップのキーと値が、配列や構造体のような複合型である場合に、データ競合検出器が適切にメモリ範囲をチェックするように変更されています。
コミット
commit 1cc2ff8fc7b3729116f43bf68f9456b8f2d0efa9
Author: Keith Randall <khr@golang.org>
Date: Mon Dec 30 12:03:56 2013 -0800
runtime: use readrange instead of read to check for races
on map keys and values which are now passed by reference.
R=dvyukov, khr
CC=golang-codereviews
https://golang.org/cl/43490044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1cc2ff8fc7b3729116f43bf68f9456b8f2d0efa9
元コミット内容
runtime: use readrange instead of read to check for races
on map keys and values which are now passed by reference.
R=dvyukov, khr
CC=golang-codereviews
https://golang.org/cl/43490044
変更の背景
Go言語のマップ(map
)は、並行処理においてデータ競合が発生しやすいデータ構造の一つです。複数のゴルーチンが同時に同じマップにアクセスし、読み書きを行うと、予期せぬ結果やプログラムのクラッシュを引き起こす可能性があります。Goランタイムには、このようなデータ競合を検出するための「レース検出器(Race Detector)」が組み込まれています。
このコミットの背景には、マップのキーや値が「参照渡し」されるようになったという変更があります。以前は値渡しであったものが参照渡しに変わったことで、レース検出器がキーや値のメモリ領域全体を正しく監視する必要が生じました。特に、キーや値が配列や構造体のような複合型である場合、その全体がアクセスされる可能性があるため、単一のポインタの読み取りだけでなく、そのメモリ範囲全体に対する読み取りを検出する必要がありました。
runtime·racereadpc
は単一のメモリ位置に対する読み取りを検出するのに対し、runtime·racereadrangepc
は指定されたメモリ範囲に対する読み取りを検出します。参照渡しされる複合型のキーや値に対して runtime·racereadpc
を使用すると、その複合型の一部が変更された場合にレース検出器がそれを検知できないという問題が発生する可能性がありました。このコミットは、この検出漏れを防ぐために、適切なレース検出関数に切り替えることを目的としています。
前提知識の解説
データ競合 (Data Race)
データ競合とは、複数の並行実行されるスレッド(Goにおいてはゴルーチン)が、同期メカニズムなしに同じメモリ位置にアクセスし、そのうち少なくとも1つのアクセスが書き込みである場合に発生する状況を指します。データ競合は、プログラムの動作を予測不能にし、デバッグを困難にするバグの主要な原因となります。
Go言語のメモリモデルでは、データ競合が発生した場合のプログラムの動作は未定義です。これは、コンパイラやCPUが命令の順序を最適化する際に、データ競合によって予期せぬ結果が生じる可能性があるためです。
Go Race Detector
Go言語には、ビルド時に-race
フラグを付けることで有効にできる組み込みのレース検出器があります。これは、実行時にデータ競合を検出し、その競合が発生した場所(ファイル名、行番号、スタックトレース)を報告するツールです。レース検出器は、メモリへのアクセス(読み書き)を監視し、同期メカニズム(ミューテックス、チャネルなど)が適切に使用されていない場合に警告を発します。
レース検出器は、プログラムの実行速度を低下させるオーバーヘッドがありますが、並行処理のバグを見つける上で非常に強力なツールです。
runtime·racereadpc
と runtime·racereadrangepc
これらはGoランタイム内部で使用される関数で、レース検出器がメモリへのアクセスを監視するために呼び出されます。
-
runtime·racereadpc(addr, pc, callpc)
:addr
: アクセスされたメモリのアドレス。pc
: アクセスを行った命令のプログラムカウンタ(PC)。callpc
:runtime·racereadpc
を呼び出した関数の呼び出し元のPC。 この関数は、単一のメモリ位置(ポインタが指す場所)に対する読み取りアクセスをレース検出器に通知します。
-
runtime·racereadrangepc(addr, size, pc, callpc)
:addr
: アクセスされたメモリ範囲の開始アドレス。size
: アクセスされたメモリ範囲のサイズ(バイト単位)。pc
,callpc
: 上記と同様。 この関数は、指定されたメモリ範囲全体に対する読み取りアクセスをレース検出器に通知します。配列や構造体のように複数のメモリ位置を占めるデータ型の場合、この関数を使用して範囲全体を監視する必要があります。
KindArray
と KindStruct
Go言語の型システムにおいて、KindArray
は配列型を、KindStruct
は構造体型を表します。これらは複数の要素やフィールドを持つ複合型であり、メモリ上では連続した領域を占めることが一般的です。
技術的詳細
このコミットの主要な変更点は、Goランタイムのマップ操作関数 (runtime·mapaccess1
, runtime·mapaccess2
, reflect·mapaccess
, runtime·mapassign1
, runtime·mapdelete
, reflect·mapassign
, reflect·mapdelete
) において、マップのキー (ak
または key
) および値 (av
または val
) に対するレース検出のロジックが修正されたことです。
具体的には、以下の条件が追加されました。
if(t->key->kind == KindArray || t->key->kind == KindStruct)
runtime·racereadrangepc(ak, t->key->size, runtime·getcallerpc(&t), runtime·mapaccess1);
else
runtime·racereadpc(ak, runtime·getcallerpc(&t), runtime·mapaccess1);
このコードスニペットは、マップのキーの型 (t->key->kind
) が KindArray
(配列)または KindStruct
(構造体)である場合に、runtime·racereadrangepc
を使用してキーのメモリ範囲全体 (t->key->size
で指定されるサイズ) をレース検出器に通知するように変更しています。それ以外の型(プリミティブ型など)の場合は、これまで通り runtime·racereadpc
を使用して単一のメモリ位置を通知します。
同様の変更が、マップの値 (av
または val
) に対しても runtime·mapassign1
および reflect·mapassign
関数で行われています。
この変更により、マップのキーや値が配列や構造体のような複合型であり、かつそれらが参照渡しされるようになった場合でも、レース検出器がその複合型全体に対する読み書きを正確に監視できるようになります。これにより、複合型の一部が並行して変更された際に発生するデータ競合を適切に検出できるようになり、レース検出器の精度と信頼性が向上します。
また、src/pkg/runtime/race/testdata/map_test.go
には、Big
という大きな構造体([17]int32
の配列を含む)をマップのキーや値として使用し、その一部に並行してアクセスするテストケースが追加されています。これらのテストは、今回の変更が正しく機能し、複合型に対するデータ競合を検出できることを検証するために書かれました。例えば、TestRaceMapLookupPartKey
では、Big
型のキーの一部 (k.x[8]
) をゴルーチン内で変更しながら、メインゴルーチンでそのキーを使ってマップをルックアップする際に、レース検出器が競合を報告するかどうかをテストしています。
コアとなるコードの変更箇所
変更は主に src/pkg/runtime/hashmap.c
に集中しています。
src/pkg/runtime/hashmap.c
runtime·mapaccess1
関数内:- キー
ak
のレース検出において、t->key->kind
がKindArray
またはKindStruct
の場合にruntime·racereadrangepc
を使用するように変更。それ以外はruntime·racereadpc
を使用。
- キー
runtime·mapaccess2
関数内:- キー
ak
のレース検出において、t->key->kind
がKindArray
またはKindStruct
の場合にruntime·racereadrangepc
を使用するように変更。それ以外はruntime·racereadpc
を使用。
- キー
reflect·mapaccess
関数内:- キー
key
のレース検出において、t->key->kind
がKindArray
またはKindStruct
の場合にruntime·racereadrangepc
を使用するように変更。それ以外はruntime·racereadpc
を使用。
- キー
runtime·mapassign1
関数内:- キー
ak
のレース検出において、t->key->kind
がKindArray
またはKindStruct
の場合にruntime·racereadrangepc
を使用するように変更。それ以外はruntime·racereadpc
を使用。 - 値
av
のレース検出において、t->elem->kind
がKindArray
またはKindStruct
の場合にruntime·racereadrangepc
を使用するように変更。それ以外はruntime·racereadpc
を使用。
- キー
runtime·mapdelete
関数内:- キー
ak
のレース検出において、t->key->kind
がKindArray
またはKindStruct
の場合にruntime·racereadrangepc
を使用するように変更。それ以外はruntime·racereadpc
を使用。
- キー
reflect·mapassign
関数内:- キー
key
のレース検出において、t->key->kind
がKindArray
またはKindStruct
の場合にruntime·racereadrangepc
を使用するように変更。それ以外はruntime·racereadpc
を使用。 - 値
val
のレース検出において、t->elem->kind
がKindArray
またはKindStruct
の場合にruntime·racereadrangepc
を使用するように変更。それ以外はruntime·racereadpc
を使用。
- キー
reflect·mapdelete
関数内:- キー
key
のレース検出において、t->key->kind
がKindArray
またはKindStruct
の場合にruntime·racereadrangepc
を使用するように変更。それ以外はruntime·racereadpc
を使用。
- キー
src/pkg/runtime/race/testdata/map_test.go
Big
構造体の定義追加:type Big struct { x [17]int32 }
- 以下の新しいテストケースの追加:
TestRaceMapLookupPartKey
TestRaceMapLookupPartKey2
TestRaceMapDeletePartKey
TestRaceMapInsertPartKey
TestRaceMapInsertPartVal
コアとなるコードの解説
変更の核となるのは、src/pkg/runtime/hashmap.c
内のマップ操作関数におけるレース検出ロジックの条件分岐です。
// 例: runtime·mapaccess1 関数内でのキーのレース検出
if(raceenabled && h != nil) {
runtime·racereadpc(h, runtime·getcallerpc(&t), runtime·mapaccess1); // マップヘッダの読み取り検出
if(t->key->kind == KindArray || t->key->kind == KindStruct) // キーが配列または構造体の場合
runtime·racereadrangepc(ak, t->key->size, runtime·getcallerpc(&t), runtime·mapaccess1); // 範囲読み取り検出
else // それ以外の型の場合
runtime·racereadpc(ak, runtime·getcallerpc(&t), runtime·mapaccess1); // 単一読み取り検出
}
このコードは、Goのレース検出器が有効 (raceenabled
) であり、マップがnilでない場合に実行されます。
- まず、マップヘッダ
h
自体への読み取りアクセスがruntime·racereadpc
を使って検出されます。 - 次に、マップのキー
ak
に対するレース検出が行われます。ここで重要なのがif
文による条件分岐です。t->key->kind == KindArray || t->key->kind == KindStruct
の条件は、マップのキーの型が配列 (KindArray
) または構造体 (KindStruct
) であるかどうかをチェックしています。これらの型は複合型であり、複数のメモリ位置を占めます。- もし条件が真であれば、
runtime·racereadrangepc(ak, t->key->size, ...)
が呼び出されます。これは、キーak
が指すアドレスからt->key->size
バイトの範囲全体に対する読み取りアクセスをレース検出器に通知します。これにより、複合型の一部が並行してアクセスされた場合でも、レース検出器がそれを検知できるようになります。 - 条件が偽であれば(つまり、キーがプリミティブ型やポインタ型など、単一のメモリ位置で表現される型の場合)、これまで通り
runtime·racereadpc(ak, ...)
が呼び出され、単一のメモリ位置に対する読み取りアクセスが検出されます。
同様のロジックが、マップの値に対しても適用されています。この変更により、Goのレース検出器は、参照渡しされる複合型のマップキーと値に対するデータ競合をより正確に検出できるようになり、並行処理のバグの発見に貢献します。
map_test.go
に追加されたテストは、この新しいロジックが期待通りに機能することを確認するためのものです。Big
構造体のような大きな複合型をキーや値として使用し、その内部の一部を並行して変更することで、レース検出器が正しく競合を報告するかどうかを検証しています。
関連リンク
- Go言語のメモリモデル: https://go.dev/ref/mem
- Go Race Detector: https://go.dev/doc/articles/race_detector
- Goのマップに関するドキュメント: https://go.dev/blog/maps
参考にした情報源リンク
- コミット情報:
/home/orange/Project/comemo/commit_data/18134.txt
- Go言語公式ドキュメント (Go Memory Model, Race Detector, Maps)
- Goソースコード (
src/pkg/runtime/hashmap.c
,src/pkg/runtime/race/testdata/map_test.go
) - Goの型システムに関する情報 (KindArray, KindStruct)
- Goの内部関数に関する一般的な知識
- データ競合に関する一般的なプログラミング知識
- GoのCL (Change List) 43490044: https://golang.org/cl/43490044 (コミットメッセージに記載)