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

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

このコミットは、Go言語のランタイムにデータ競合検出機能(Data Race Detector)を統合するための重要な変更を含んでいます。具体的には、Goプログラムの実行中に発生する可能性のあるデータ競合を検出し、開発者に警告するための基盤となるランタイムレベルの変更が導入されています。

変更されたファイルは以下の通りです。

  • src/pkg/runtime/cgocall.c: Cgo呼び出しにおけるデータ競合検出の追加。
  • src/pkg/runtime/chan.c: チャネル操作におけるデータ競合検出の追加。
  • src/pkg/runtime/hashmap.c: マップ操作におけるデータ競合検出の追加。
  • src/pkg/runtime/malloc.goc: メモリ割り当てと解放におけるデータ競合検出の追加。
  • src/pkg/runtime/mgc0.c: ガベージコレクション関連の処理におけるデータ競合検出の追加。
  • src/pkg/runtime/proc.c: プロセスおよびゴルーチン管理におけるデータ競合検出の追加。
  • src/pkg/runtime/race.c: データ競合検出器のC言語実装。
  • src/pkg/runtime/race.go: データ競合検出器のGo言語API。
  • src/pkg/runtime/race.h: データ競合検出器のヘッダーファイル。
  • src/pkg/runtime/race/race.go: ThreadSanitizer (TSan) とのGo言語バインディング。
  • src/pkg/runtime/race/race_darwin_amd64.syso: Darwin (macOS) AMD64 用のTSanライブラリ。
  • src/pkg/runtime/race/race_linux_amd64.syso: Linux AMD64 用のTSanライブラリ。
  • src/pkg/runtime/race0.c: データ競合検出が無効な場合のスタブ実装。
  • src/pkg/runtime/runtime.h: ランタイムの共通ヘッダーファイルへの変更。
  • src/pkg/runtime/slice.c: スライス操作におけるデータ競合検出の追加。
  • src/pkg/runtime/time.goc: タイマー操作におけるデータ競合検出の追加。

これらの変更は、Goプログラムの並行処理における潜在的なバグを早期に発見し、修正するために不可欠なツールを提供します。

コミット

commit 2f6cbc74f18542b0f79374a2210e420b9500218f
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Sun Oct 7 22:05:32 2012 +0400

    race: runtime changes
    This is a part of a bigger change that adds data race detection feature:
    https://golang.org/cl/6456044
    
    R=rsc
    CC=gobot, golang-dev
    https://golang.org/cl/6535050

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

https://github.com/golang/go/commit/2f6cbc74f18542b0f79374a2210e420b9500218f

元コミット内容

race: runtime changes
This is a part of a bigger change that adds data race detection feature:
https://golang.org/cl/6456044

R=rsc
CC=gobot, golang-dev
https://golang.org/cl/6535050

変更の背景

このコミットは、Go言語にデータ競合検出機能を追加するという、より大きな変更の一部です。データ競合は、複数のゴルーチンが共有メモリに同時にアクセスし、少なくとも1つのアクセスが書き込みであるにもかかわらず、それらのアクセスが同期メカニズムによって保護されていない場合に発生する並行処理のバグです。このような競合は、プログラムの予測不能な動作、データの破損、クラッシュなどを引き起こす可能性があります。

Go言語は並行処理を強力にサポートしていますが、それゆえにデータ競合のリスクも伴います。開発者がデータ競合を容易に特定し、修正できるようにするために、Goチームはランタイムレベルでデータ競合を検出するツールを開発しました。このコミットは、その検出器をGoランタイムに統合するための基盤となる変更を導入しています。コミットメッセージに記載されている https://golang.org/cl/6456044 は、このデータ競合検出機能全体の導入に関する主要な変更リスト(Change List)を示しています。

前提知識の解説

データ競合 (Data Race)

データ競合とは、並行プログラミングにおいて、複数のスレッド(Goにおいてはゴルーチン)が同じメモリ位置に同時にアクセスし、そのうち少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが適切な同期メカニズム(ミューテックス、チャネルなど)によって保護されていない場合に発生する競合状態のことです。

データ競合の例:

var counter int

func increment() {
    for i := 0; i < 100000; i++ {
        counter++ // 競合が発生する可能性のあるアクセス
    }
}

func main() {
    go increment()
    go increment()
    // counter の最終的な値は予測不能になる
}

この例では、counter 変数へのアクセスが複数のゴルーチンから同時に行われ、counter++ は「読み込み」「インクリメント」「書き込み」の3つの操作からなるため、データ競合が発生します。結果として counter の最終的な値は期待通りにならない可能性があります。

データ競合検出器 (Data Race Detector)

データ競合検出器は、実行時にプログラムのメモリアクセスを監視し、データ競合のパターンを特定するツールです。Go言語のデータ競合検出器は、Googleが開発した動的解析ツールであるThreadSanitizer (TSan) をベースにしています。

TSanの仕組み:

TSanは、コンパイル時にプログラムのバイナリコードにインストゥルメンテーション(計測コードの埋め込み)を行います。これにより、すべてのメモリ読み書き、ゴルーチンの開始/終了、同期プリミティブ(ミューテックス、チャネルなど)の操作が記録されます。実行時にこれらのイベントを分析し、データ競合のルール(例えば、異なるゴルーチンからの非同期な読み書きアクセス)に違反するパターンを検出すると、警告を生成します。

Go言語では、go build -racego run -racego test -race などのコマンドに -race フラグを追加することで、このデータ競合検出器を有効にできます。有効にすると、コンパイルされたバイナリにはTSanのランタイムライブラリがリンクされ、実行時にデータ競合が検出された場合に詳細なレポートが出力されます。

技術的詳細

このコミットは、GoランタイムにTSanを統合するための低レベルな変更を多数含んでいます。主な技術的ポイントは以下の通りです。

  1. race.crace.h の導入:

    • src/pkg/runtime/race.c は、GoランタイムからTSanのC言語APIを呼び出すためのラッパー関数を提供します。例えば、runtime·racereadruntime·racewriteruntime·racemallocruntime·racefree などがあります。
    • これらの関数は、Goのメモリ操作や同期イベントをTSanに通知する役割を担います。
    • src/pkg/runtime/race.h は、これらのラッパー関数のプロトタイプと、raceenabled というコンパイル時定数を定義しています。raceenabled は、-race フラグが指定された場合に 1 となり、それ以外の場合は 0 となります。これにより、競合検出コードの有効/無効を切り替えることができます。
  2. race.gorace/race.go の導入:

    • src/pkg/runtime/race.go は、Go言語からデータ競合検出器のAPIを呼び出すための公開関数(RaceDisable, RaceEnable, RaceAcquire, RaceRelease など)を定義しています。これらは主に、特定のコードブロックで競合検出を一時的に無効にしたり、カスタム同期イベントをTSanに通知したりするために使用されます。
    • src/pkg/runtime/race/race.go は、Cgoを介してTSanのネイティブC関数(__tsan_init, __tsan_read, __tsan_write など)を呼び出すためのGo言語バインディングを提供します。このファイルは、+build race タグによって、-race フラグが指定された場合にのみコンパイルされます。
  3. ランタイムのインストゥルメンテーション:

    • cgocall.c, chan.c, hashmap.c, malloc.goc, mgc0.c, proc.c, slice.c, time.goc などのGoランタイムのコア部分に、raceenabled のチェックとTSanへの通知(runtime·racereadpc, runtime·racewritepc, runtime·racerelease, runtime·raceacquire など)が追加されています。
    • これにより、チャネルの送受信、マップへのアクセス、メモリの割り当て/解放、ゴルーチンの開始/終了、Cgo呼び出しなど、並行処理に関連する重要な操作がTSanによって監視されるようになります。
    • 例えば、チャネルの送受信では、チャネルバッファへのアクセスが runtime·racereleaseruntime·raceacquire でラップされ、同期イベントとしてTSanに通知されます。これにより、チャネルを介したデータ転送が正しく同期されているかをTSanが検証できます。
  4. race_darwin_amd64.syso および race_linux_amd64.syso:

    • これらは、それぞれDarwin (macOS) とLinuxのAMD64アーキテクチャ用のTSanランタイムライブラリのバイナリファイルです。-race フラグでビルドする際に、これらのライブラリがGoプログラムに静的にリンクされます。
  5. race0.c の導入:

    • src/pkg/runtime/race0.c は、-race フラグが指定されていない場合にコンパイルされるスタブ実装です。これにより、競合検出機能が有効でないビルドでは、TSan関連の関数呼び出しが何もしない空の関数に置き換えられ、オーバーヘッドが発生しないようになっています。

これらの変更により、GoプログラムはTSanの強力なデータ競合検出能力を活用できるようになり、並行処理のバグを効率的に特定・修正することが可能になります。

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

src/pkg/runtime/chan.c におけるチャネル操作のインストゥルメンテーション

チャネルの送受信操作に raceenabled のチェックと runtime·racereadpc, runtime·racerelease, runtime·raceacquire, racesync の呼び出しが追加されています。

// runtime·chansend 関数内
 void
 runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)
 {
     // ...
     if(raceenabled)
         runtime·racereadpc(c, pc); // チャネルへの読み込みアクセスをTSanに通知
     // ...
     if(sg != nil) { // 受信側ゴルーチンが存在する場合
         if(raceenabled)
             racesync(c, sg); // 送信と受信の同期イベントをTSanに通知
         // ...
     } else { // バッファリングされたチャネルへの送信
         // ...
         if(raceenabled)
             runtime·racerelease(chanbuf(c, c->sendx)); // バッファへの書き込みをTSanに通知
         // ...
     }
     // ...
 }

// runtime·chanrecv 関数内
 void
 runtime·chanrecv(ChanType *t, Hchan* c, byte *ep, bool *selected, bool *received)
 {
     // ...
     if(sg != nil) { // 送信側ゴルーチンが存在する場合
         if(raceenabled)
             racesync(c, sg); // 受信と送信の同期イベントをTSanに通知
         // ...
     } else { // バッファリングされたチャネルからの受信
         // ...
         if(raceenabled)
             runtime·raceacquire(chanbuf(c, c->recvx)); // バッファからの読み込みをTSanに通知
         // ...
     }
     // ...
     if(raceenabled)
         runtime·raceacquire(c); // チャネルへの読み込みアクセスをTSanに通知 (クローズ時)
     // ...
 }

// 新しく追加された racesync 関数
 static void
 racesync(Hchan *c, SudoG *sg)
 {
     runtime·racerelease(chanbuf(c, 0));
     runtime·raceacquireg(sg->g, chanbuf(c, 0));
     runtime·racereleaseg(sg->g, chanbuf(c, 0));
     runtime·raceacquire(chanbuf(c, 0));
 }

src/pkg/runtime/malloc.goc におけるメモリ割り当て/解放のインストゥルメンテーション

runtime·mallocgc (メモリ割り当て) と runtime·free (メモリ解放) に racemallocracefree の呼び出しが追加されています。

// runtime·mallocgc 関数内
 void*
 runtime·mallocgc(uintptr size, uint32 flag, int32 dogc, int32 zeroed)
 {
     // ...
     if(raceenabled) {
         runtime·racemalloc(v, size, m->racepc); // メモリ割り当てをTSanに通知
         m->racepc = nil;
     }
     return v;
 }

// runtime·free 関数内
 void
 runtime·free(void *v)
 {
     // ...
     if(raceenabled)
         runtime·racefree(v); // メモリ解放をTSanに通知
     // ...
 }

src/pkg/runtime/proc.c におけるゴルーチン管理のインストゥルメンテーション

runtime·schedinit (スケジューラ初期化), runtime·main (メインゴルーチン終了), schedule (ゴルーチン終了), runtime·newproc1 (新規ゴルーチン作成) に raceinit, racefini, racegoend, racegostart の呼び出しが追加されています。

// runtime·schedinit 関数内
 void
 runtime·schedinit(void)
 {
     // ...
     if(raceenabled)
         runtime·raceinit(); // TSanの初期化
 }

// runtime·main 関数内
 void
 runtime·main(void)
 {
     // ...
     main·main();
     if(raceenabled)
         runtime·racefini(); // TSanの終了処理
     // ...
 }

// schedule 関数内 (Gmoribund ステータスの場合)
 void
 schedule(G *gp)
 {
     // ...
     case Gmoribund:
         if(raceenabled)
             runtime·racegoend(gp->goid); // ゴルーチン終了をTSanに通知
         // ...
 }

// runtime·newproc1 関数内
 void
 runtime·newproc1(byte *fn, byte *argp, int32 narg, int32 nret, void *callerpc)
 {
     // ...
     goid = runtime·xadd((uint32*)&runtime·sched.goidgen, 1);
     if(raceenabled)
         runtime·racegostart(goid, callerpc); // 新規ゴルーチン開始をTSanに通知
     // ...
 }

src/pkg/runtime/race.c (新規ファイル)

TSanのC言語APIをラップする関数群が定義されています。

// runtime·racewrite 関数
 void
 runtime·racewrite(uintptr addr)
 {
     if(!onstack(addr)) { // スタック上のアドレスでない場合のみ
         m->racecall = true;
         runtime∕race·Write(g->goid-1, (void*)addr, runtime·getcallerpc(&addr)); // TSanのWrite関数を呼び出し
         m->racecall = false;
     }
 }

// runtime·raceread 関数
 void
 runtime·raceread(uintptr addr)
 {
     if(!onstack(addr)) { // スタック上のアドレスでない場合のみ
         m->racecall = true;
         runtime∕race·Read(g->goid-1, (void*)addr, runtime·getcallerpc(&addr)); // TSanのRead関数を呼び出し
         m->racecall = false;
     }
 }

コアとなるコードの解説

上記のコード変更は、GoランタイムがTSanと連携してデータ競合を検出する方法を示しています。

  • チャネル操作 (chan.c):

    • runtime·chansendruntime·chanrecv 関数内で、チャネルへのアクセス(読み書き)や、チャネルを介したゴルーチン間の同期イベント(racesync)が発生するたびに、runtime·racereadpc, runtime·racewritepc, runtime·racerelease, runtime·raceacquire といったTSanへの通知関数が呼び出されます。
    • racesync 関数は、チャネルを介したゴルーチン間のハンドオフ(データの受け渡し)をTSanに正確に伝えるための重要な役割を担っています。これにより、TSanはチャネルが正しく同期メカニズムとして機能しているかを検証できます。
    • chanbuf(c, c->sendx)chanbuf(c, c->recvx) は、チャネルの内部バッファへのポインタを生成し、そのメモリ領域へのアクセスをTSanに監視させています。
  • メモリ割り当て/解放 (malloc.goc):

    • runtime·mallocgcruntime·free 関数内で、それぞれ runtime·racemallocruntime·racefree が呼び出されます。これにより、TSanはヒープメモリの割り当てと解放を追跡し、解放後の使用(use-after-free)などのメモリ関連の競合を検出できるようになります。
  • ゴルーチン管理 (proc.c):

    • runtime·schedinitruntime·raceinit が呼び出され、TSanランタイムが初期化されます。
    • runtime·main の終了時に runtime·racefini が呼び出され、TSanランタイムが終了処理を行います。
    • runtime·newproc1 で新しいゴルーチンが作成される際に runtime·racegostart が呼び出され、TSanに新しいゴルーチンの開始を通知します。
    • ゴルーチンが終了する際に runtime·racegoend が呼び出され、TSanにゴルーチンの終了を通知します。
    • これらの通知により、TSanはゴルーチンのライフサイクルを正確に追跡し、異なるゴルーチン間でのメモリアクセスの競合を検出できます。
  • race.c のラッパー関数:

    • runtime·racewriteruntime·raceread のような関数は、GoランタイムのCコードからTSanのネイティブ関数を呼び出すための橋渡しをします。
    • onstack(addr) のチェックは、アクセス対象のメモリがスタック上にあるかどうかを判断しています。TSanは通常、スタック上のローカル変数へのアクセスは競合の対象外とみなすため、このチェックによって不要なインストゥルメンテーションを避けています。
    • m->racecall = true/false の設定は、TSanの内部処理中にTSan自身がさらにTSanの関数を呼び出すことによる無限ループやデッドロックを防ぐためのものです。これにより、TSanの内部処理がTSanによって監視されないようにしています。

これらの変更は、Goプログラムの実行パスの多くの重要なポイントにTSanの計測コードを挿入し、並行処理のババグを検出するための包括的なフレームワークを構築しています。

関連リンク

参考にした情報源リンク