[インデックス 15222] ファイルの概要
このコミットは、Goランタイムのバイトスライスおよびruneスライスから文字列への変換処理 (slicebytetostring
および slicerunetostring
) に、データ競合検出のための計測(instrumentation)を追加するものです。これにより、これらの変換処理中に発生しうるデータ競合をGoの競合検出器が正確に報告できるようになります。
コミット
commit 4a524311f4d538a2c5a45d56286fdefbd2cf1c7a
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed Feb 13 18:29:59 2013 +0400
runtime: instrument slicebytetostring for race detection
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/7322068
---
src/pkg/runtime/race/testdata/slice_test.go | 20 ++++++++++++++++++++\n src/pkg/runtime/string.goc | 12 ++++++++++++\n 2 files changed, 32 insertions(+)
diff --git a/src/pkg/runtime/race/testdata/slice_test.go b/src/pkg/runtime/race/testdata/slice_test.go
index 1440a5f13e..773463662b 100644
--- a/src/pkg/runtime/race/testdata/slice_test.go
+++ b/src/pkg/runtime/race/testdata/slice_test.go
@@ -443,3 +443,23 @@ func TestRaceSliceIndexAccess2(t *testing.T) {
_ = s[v]
<-c
}
+
+func TestRaceSliceByteToString(t *testing.T) {
+ c := make(chan string)
+ s := make([]byte, 10)
+ go func() {
+ c <- string(s)
+ }()
+ s[0] = 42
+ <-c
+}
+
+func TestRaceSliceRuneToString(t *testing.T) {
+ c := make(chan string)
+ s := make([]rune, 10)
+ go func() {
+ c <- string(s)
+ }()
+ s[9] = 42
+ <-c
+}
diff --git a/src/pkg/runtime/string.goc b/src/pkg/runtime/string.goc
index cafcdb6ced..c0d3f2bde9 100644
--- a/src/pkg/runtime/string.goc
+++ b/src/pkg/runtime/string.goc
@@ -6,6 +6,7 @@ package runtime
#include "runtime.h"
#include "arch_GOARCH.h"
#include "malloc.h"
+#include "race.h"
String runtime·emptystring;
@@ -271,6 +272,12 @@ func intstring(v int64) (s String) {
}
func slicebytetostring(b Slice) (s String) {
+ void *pc;
+
+ if(raceenabled) {
+ pc = runtime·getcallerpc(&b);
+ runtime·racereadrangepc(b.array, b.len, 1, pc, runtime·slicebytetostring);
+ }
s = gostringsize(b.len);
runtime·memmove(s.str, b.array, s.len);
}
@@ -286,7 +293,12 @@ func slicerunetostring(b Slice) (s String) {
intgo siz1, siz2, i;
int32 *a;
byte dum[8];
+ void *pc;
+ if(raceenabled) {
+ pc = runtime·getcallerpc(&b);
+ runtime·racereadrangepc(b.array, b.len*sizeof(*a), sizeof(*a), pc, runtime·slicerunetostring);
+ }
a = (int32*)b.array;
siz1 = 0;
for(i=0; i<b.len; i++) {
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4a524311f4d538a2c5a45d56286fdefbd2cf1c7a
元コミット内容
このコミットの元々の内容は、Goランタイムにおけるバイトスライス ([]byte
) およびruneスライス ([]rune
) から文字列への変換関数 (slicebytetostring
および slicerunetostring
) に、Goの競合検出器(Race Detector)がこれらのメモリ読み取り操作を監視できるようにするためのコードを追加することです。具体的には、raceenabled
フラグが有効な場合に、スライスが文字列に変換される際にそのスライスのメモリ範囲が読み取られたことを競合検出器に通知する runtime·racereadrangepc
関数を呼び出すように変更されています。これに伴い、競合検出器がこれらのケースを検出できることを検証するためのテストケースも追加されています。
変更の背景
Goの競合検出器は、並行処理におけるデータ競合(複数のゴルーチンが同期なしに同じメモリ位置にアクセスし、少なくとも1つが書き込みである場合)を特定するための強力なツールです。しかし、ランタイム内部の特定の操作、特にスライスから文字列への変換のような低レベルなメモリ操作は、競合検出器の監視対象から漏れる可能性がありました。
string(s)
のような変換は、内部的にはスライスの内容を新しい文字列にコピーする操作を含みます。もし、あるゴルーチンがスライスを文字列に変換しようとしている最中に、別のゴルーチンがそのスライスの内容を書き換えた場合、これは典型的なデータ競合となります。競合検出器がこの種の競合を検出できないと、開発者は潜在的なバグを見逃すことになります。
このコミットの背景には、このようなランタイム内部の操作が競合検出器によって適切に監視されるようにし、Goプログラムの並行性の安全性をさらに高めるという目的があります。特に、string(s)
変換は頻繁に行われる操作であるため、その安全性は非常に重要です。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
Goランタイム (Go Runtime): Goプログラムの実行環境全体を指します。これには、ガベージコレクション、ゴルーチン(軽量スレッド)のスケジューリング、メモリ管理、チャネル操作、プリミティブな型変換など、Go言語の低レベルな動作を司るコンポーネントが含まれます。GoランタイムはC言語とGo言語(一部アセンブリ)で実装されており、
src/pkg/runtime
ディレクトリにそのソースコードがあります。 -
Go競合検出器 (Go Race Detector): Go 1.1で導入された動的解析ツールです。プログラムの実行中にデータ競合を検出します。データ競合とは、以下の3つの条件がすべて満たされる場合に発生するバグです。
- 2つ以上のゴルーチンが同じメモリ位置に同時にアクセスする。
- それらのアクセスのうち少なくとも1つが書き込みである。
- それらのアクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。
競合検出器は、メモリアクセスを計測(instrumentation)することで動作します。つまり、プログラムのコンパイル時に追加のコードが挿入され、メモリの読み書き操作を監視し、競合パターンを検出します。
go run -race
やgo build -race
コマンドで有効にできます。
-
スライス (Slice): Goにおける可変長シーケンス型です。内部的には、基盤となる配列へのポインタ、長さ(
len
)、容量(cap
)の3つの要素で構成されます。スライスは配列の一部を参照するビューのようなものであり、複数のスライスが同じ基盤配列を共有することが可能です。 -
文字列 (String): Goにおける不変なバイトシーケンスです。文字列は一度作成されると内容を変更できません。
string(s)
のようにスライスから文字列への変換を行うと、通常はスライスの内容が新しい文字列のメモリ領域にコピーされます。 -
slicebytetostring
およびslicerunetostring
: これらはGoランタイム内部で使用される関数で、それぞれ[]byte
からstring
へ、[]rune
からstring
への変換を処理します。これらの関数はGo言語のコードからは直接呼び出されず、コンパイラがstring(byteSlice)
やstring(runeSlice)
のような変換を見つけると、これらのランタイム関数への呼び出しに置き換えます。 -
runtime·getcallerpc
: Goランタイム内部の関数で、現在の関数の呼び出し元のプログラムカウンタ(Program Counter, PC)を取得します。PCは、実行中の命令のアドレスを示すレジスタであり、競合検出器が競合を報告する際に、どのコード行で競合が発生したかを特定するために使用されます。 -
runtime·racereadrangepc
: Go競合検出器の内部関数です。特定のメモリ範囲が読み取られたことを競合検出器に通知するために使用されます。引数には、読み取られたメモリ範囲の開始アドレス、長さ、要素サイズ、呼び出し元のPC、そして関連する関数ポインタなどが渡されます。これにより、競合検出器はメモリの読み取り履歴を追跡し、後続の書き込み操作との競合を検出できます。
技術的詳細
このコミットの核心は、Goランタイムがスライスから文字列への変換を行う際に、そのスライスのメモリ領域に対する「読み取り」操作を競合検出器に明示的に通知する点にあります。
Goの string(s)
変換は、スライス s
の内容を新しい文字列のメモリ領域にコピーします。このコピー操作は、スライスの内容を読み取ることに他なりません。もし、この読み取り操作が進行中に、別のゴルーチンが同じスライスのメモリ領域に書き込みを行った場合、それはデータ競合となります。
競合検出器は、通常、Go言語レベルでの変数アクセスを監視しますが、ランタイム内部の低レベルなメモリ操作は、明示的な計測なしには検出が難しい場合があります。このコミットは、まさにそのギャップを埋めるものです。
具体的には、src/pkg/runtime/string.goc
内の slicebytetostring
および slicerunetostring
関数に以下のロジックが追加されました。
if(raceenabled) {
void *pc = runtime·getcallerpc(&b); // 呼び出し元のPCを取得
// スライスのメモリ範囲が読み取られたことを競合検出器に通知
runtime·racereadrangepc(b.array, b.len, 1, pc, runtime·slicebytetostring);
}
(slicerunetostring
の場合は b.len
の代わりに b.len*sizeof(*a)
を使用し、runeのサイズを考慮します。)
raceenabled
は、競合検出器が有効になっているかどうかを示すグローバルフラグです。競合検出器が有効な場合にのみ、計測コードが実行されます。これにより、競合検出器が無効な場合のオーバーヘッドを避けます。runtime·getcallerpc(&b)
は、現在の関数(slicebytetostring
またはslicerunetostring
)を呼び出したゴルーチンのプログラムカウンタを取得します。これは、競合が検出された際に、ユーザーが書いたどのコードが原因で競合が発生したかを特定するために重要です。runtime·racereadrangepc
は、競合検出器にメモリ読み取りイベントを報告します。b.array
: スライスの基盤となる配列の先頭アドレス。b.len
(またはb.len*sizeof(*a)
): 読み取られるメモリ範囲のバイト数。1
(またはsizeof(*a)
): 読み取られる各要素のサイズ(バイトスライスでは1バイト、runeスライスではruneのサイズ)。pc
: 呼び出し元のプログラムカウンタ。runtime·slicebytetostring
(またはruntime·slicerunetostring
): 競合検出器がレポートを生成する際に、どのランタイム関数が関与したかを示すための情報。
この変更により、string(s)
変換が実行されると、競合検出器はスライスの基盤となるメモリ領域に対する読み取り操作を記録します。もし、別のゴルーチンが同じメモリ領域に対して同期なしに書き込みを行った場合、競合検出器はそれを検出し、詳細なレポートを出力するようになります。
src/pkg/runtime/race/testdata/slice_test.go
に追加されたテストケース (TestRaceSliceByteToString
と TestRaceSliceRuneToString
) は、この計測が正しく機能することを確認するためのものです。これらのテストは、意図的にデータ競合を引き起こすように設計されています。
func TestRaceSliceByteToString(t *testing.T) {
c := make(chan string)
s := make([]byte, 10)
go func() {
c <- string(s) // ゴルーチンA: スライスsを文字列に変換(読み取り)
}()
s[0] = 42 // ゴルーチンB: スライスsの要素を書き換え(書き込み)
<-c
}
このテストでは、メインゴルーチンが s[0] = 42
でスライス s
を書き換えるのと同時に、別のゴルーチンが string(s)
でスライス s
を読み取ろうとします。同期メカニズムがないため、これはデータ競合です。このコミットの変更がなければ、競合検出器はこの競合を検出できない可能性がありましたが、変更後は検出できるようになります。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、以下の2つのファイルに集中しています。
-
src/pkg/runtime/race/testdata/slice_test.go
:TestRaceSliceByteToString
関数が追加されました。このテストは、[]byte
スライスを文字列に変換する際に発生する可能性のあるデータ競合をシミュレートし、競合検出器がそれを捕捉できることを検証します。TestRaceSliceRuneToString
関数が追加されました。同様に、[]rune
スライスを文字列に変換する際のデータ競合を検証します。
-
src/pkg/runtime/string.goc
:#include "race.h"
が追加され、競合検出器関連の定義がインクルードされるようになりました。func slicebytetostring(b Slice) (s String)
関数内に、raceenabled
が真の場合にruntime·racereadrangepc
を呼び出すコードが追加されました。func slicerunetostring(b Slice) (s String)
関数内に、同様にraceenabled
が真の場合にruntime·racereadrangepc
を呼び出すコードが追加されました。
コアとなるコードの解説
src/pkg/runtime/race/testdata/slice_test.go
の変更
// TestRaceSliceByteToString は、バイトスライスから文字列への変換におけるデータ競合をテストします。
func TestRaceSliceByteToString(t *testing.T) {
c := make(chan string) // 文字列を送信するためのチャネル
s := make([]byte, 10) // 10バイトのスライスを作成
// 新しいゴルーチンを起動
go func() {
// スライスsを文字列に変換し、チャネルcに送信
// この操作はスライスsのメモリを読み取ります
c <- string(s)
}()
// メインゴルーチンで、スライスsの最初の要素を書き換える
// これは、上記のゴルーチンでの読み取りと同時に発生する可能性があり、データ競合を引き起こします
s[0] = 42
// ゴルーチンからの文字列がチャネルに送信されるのを待つ
// これにより、両方の操作が完了するまでテストがブロックされます
<-c
}
// TestRaceSliceRuneToString は、runeスライスから文字列への変換におけるデータ競合をテストします。
func TestRaceSliceRuneToString(t *testing.T) {
c := make(chan string) // 文字列を送信するためのチャネル
s := make([]rune, 10) // 10個のruneのスライスを作成
// 新しいゴルーチンを起動
go func() {
// スライスsを文字列に変換し、チャネルcに送信
// この操作はスライスsのメモリを読み取ります
c <- string(s)
}()
// メインゴルーチンで、スライスsの最後の要素を書き換える
// これは、上記のゴルーチンでの読み取りと同時に発生する可能性があり、データ競合を引き起こします
s[9] = 42
// ゴルーチンからの文字列がチャネルに送信されるのを待つ
<-c
}
これらのテストは、Goの競合検出器が有効な状態で実行された場合、string(s)
変換中のスライス読み取りと、別のゴルーチンによるスライス書き込みとの間でデータ競合を報告することを確認するために設計されています。
src/pkg/runtime/string.goc
の変更
// race.h ヘッダーをインクルード。競合検出器関連の定義が含まれます。
#include "race.h"
// slicebytetostring はバイトスライスを文字列に変換します。
func slicebytetostring(b Slice) (s String) {
void *pc; // プログラムカウンタを格納するためのポインタ
// 競合検出器が有効な場合のみ以下のコードを実行
if(raceenabled) {
// 呼び出し元のプログラムカウンタを取得
pc = runtime·getcallerpc(&b);
// 競合検出器に、スライスのメモリ範囲が読み取られたことを通知
// b.array: スライスの基底アドレス
// b.len: 読み取られるバイト数
// 1: 各要素のサイズ(バイトなので1)
// pc: 呼び出し元のPC
// runtime·slicebytetostring: 競合レポートのためのコンテキスト情報
runtime·racereadrangepc(b.array, b.len, 1, pc, runtime·slicebytetostring);
}
// 元々の文字列変換ロジック
s = gostringsize(b.len);
runtime·memmove(s.str, b.array, s.len);
}
// slicerunetostring はruneスライスを文字列に変換します。
func slicerunetostring(b Slice) (s String) {
// ... (既存の変数宣言) ...
void *pc; // プログラムカウンタを格納するためのポインタ
// 競合検出器が有効な場合のみ以下のコードを実行
if(raceenabled) {
// 呼び出し元のプログラムカウンタを取得
pc = runtime·getcallerpc(&b);
// 競合検出器に、runeスライスのメモリ範囲が読み取られたことを通知
// b.array: スライスの基底アドレス
// b.len*sizeof(*a): 読み取られるバイト数(runeは通常4バイトなので、要素数×4)
// sizeof(*a): 各要素のサイズ(runeのサイズ)
// pc: 呼び出し元のPC
// runtime·slicerunetostring: 競合レポートのためのコンテキスト情報
runtime·racereadrangepc(b.array, b.len*sizeof(*a), sizeof(*a), pc, runtime·slicerunetostring);
}
// ... (元々の文字列変換ロジック) ...
}
この変更により、string(byteSlice)
や string(runeSlice)
のようなGoコードが実行されるたびに、Goランタイムは内部的に slicebytetostring
または slicerunetostring
を呼び出します。そして、競合検出器が有効な場合、これらの関数は runtime·racereadrangepc
を呼び出して、スライスの基盤となるメモリ領域が読み取られたことを競合検出器に通知します。これにより、競合検出器は、この読み取り操作と、他のゴルーチンによる同じメモリ領域への書き込み操作との間の潜在的な競合を正確に特定できるようになります。
関連リンク
- Go Race Detector の公式ドキュメント: https://go.dev/doc/articles/race_detector
- Goのソースコードリポジトリ: https://github.com/golang/go
- このコミットのGo CL (Code Review): https://golang.org/cl/7322068
参考にした情報源リンク
- Go Race Detector の仕組みに関する記事やドキュメント
- Goランタイムの内部構造に関する資料
- Go言語のスライスと文字列の内部表現に関する情報
- Goのソースコード(特に
src/pkg/runtime
ディレクトリ) - Goのテストフレームワークに関するドキュメント
- Dmitriy Vyukov氏のGo競合検出器に関する発表やブログ記事