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

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

このコミットは、Go言語のビルドツールであるcmd/goにおけるデータ競合の問題を修正するものです。具体的には、src/cmd/go/build.goファイルに2行のコードが追加され、ビルド中のディレクトリ作成処理における並行アクセス時の安全性が確保されました。

コミット

cmd/go: fix data race during build
Fixes #2695.

R=golang-dev, mpimenov, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/5545052

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

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

元コミット内容

cmd/go: fix data race during build
Fixes #2695.

R=golang-dev, mpimenov, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/5545052

変更の背景

Go言語のビルドプロセスにおいて、複数の並行処理(ゴルーチン)が同時にディレクトリを作成しようとした際に、データ競合(Data Race)が発生する可能性がありました。データ競合は、複数のゴルーチンが同時に同じメモリ領域にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生する競合状態です。このような状況では、プログラムの動作が予測不能になり、クラッシュや不正な結果を引き起こす可能性があります。

この問題は、GoのIssueトラッカーで#2695として報告されていました。ビルドプロセス中にmkdir(ディレクトリ作成)操作が並行して行われることで、内部の状態管理(例えば、既にディレクトリが存在するかどうかをキャッシュするマップなど)が正しく同期されず、競合状態に陥っていたと考えられます。このコミットは、この特定のデータ競合を解消し、ビルドプロセスの安定性と信頼性を向上させることを目的としています。

前提知識の解説

Go言語の並行処理

Go言語は、軽量なスレッドである「ゴルーチン(goroutine)」と、ゴルーチン間の安全な通信を可能にする「チャネル(channel)」を言語レベルでサポートしており、並行処理を容易に記述できることが特徴です。

  • ゴルーチン (Goroutine): goキーワードを使って関数を呼び出すことで、新しいゴルーチンが生成され、その関数が他の処理と並行して実行されます。OSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。
  • チャネル (Channel): ゴルーチン間で値を送受信するためのパイプのようなものです。チャネルを介した通信は同期的に行われるため、データ競合を防ぐための主要なメカニズムの一つとなります。

データ競合 (Data Race)

データ競合は、並行プログラミングにおける最も一般的なバグの一つです。以下の3つの条件がすべて満たされたときに発生します。

  1. 複数のゴルーチンが同時に同じメモリ領域にアクセスする。
  2. 少なくとも1つのアクセスが書き込み操作である。
  3. これらのアクセスが同期メカニズムによって保護されていない。

データ競合が発生すると、プログラムの動作は「未定義(undefined behavior)」となります。これは、プログラムがクラッシュしたり、誤った結果を生成したり、あるいは一見正しく動作しているように見えても、特定の条件下で予期せぬ問題を引き起こす可能性があることを意味します。デバッグが非常に困難な種類のバグです。

ミューテックス (Mutex)

ミューテックス(Mutual Exclusionの略)は、データ競合を防ぐための最も基本的な同期プリミティブの一つです。共有リソースへのアクセスを排他的に制御するために使用されます。Go言語では、syncパッケージのsync.Mutex型がミューテックスを提供します。

  • sync.Mutex: ゼロ値が有効なミューテックスです。
  • Lock()メソッド: ミューテックスをロックします。既にロックされている場合は、ロックが解放されるまで現在のゴルーチンはブロックされます。
  • Unlock()メソッド: ミューテックスをアンロックします。ロックを保持しているゴルーチンのみがアンロックできます。

ミューテックスを使用する際の一般的なパターンは、共有リソースにアクセスするコードブロックの開始時にLock()を呼び出し、終了時にUnlock()を呼び出すことです。Goでは、deferキーワードと組み合わせることで、関数の終了時に確実にUnlock()が呼び出されるようにすることが推奨されます。

import "sync"

var mu sync.Mutex
var sharedResource int

func updateSharedResource() {
    mu.Lock()   // ロックを取得
    defer mu.Unlock() // 関数終了時にロックを解放することを保証
    sharedResource++ // 共有リソースへの安全なアクセス
}

cmd/go

cmd/goは、Go言語のソースコードをビルド、テスト、インストール、フォーマットなどを行うためのコマンドラインツールです。Go開発者にとって最も基本的なツールであり、Goプログラムのコンパイルや実行の裏側で動作しています。このツール自体もGo言語で書かれており、複雑なビルドプロセスを管理するために内部で並行処理を利用しています。

技術的詳細

このコミットで修正されたデータ競合は、src/cmd/go/build.goファイル内のbuilder構造体のmkdirメソッドで発生していました。builder構造体は、Goのビルドプロセス全体の状態を管理する役割を担っており、そのインスタンスは複数のゴルーチンからアクセスされる可能性があります。

mkdirメソッドは、指定されたディレクトリを作成する責任を負っています。このメソッドの内部には、b.mkdirCacheというマップがあり、これは既に作成されたディレクトリをキャッシュして、重複するmkdir呼び出しをスキップするためのものです。

問題は、複数のゴルーチンが同時にbuilder.mkdirを呼び出した場合に発生しました。b.mkdirCacheは共有リソースであり、複数のゴルーチンが同時にこのマップを読み書きしようとすると、同期メカニズムがないためにデータ競合が発生します。例えば、あるゴルーチンがマップに書き込んでいる最中に別のゴルーチンがマップを読み取ろうとすると、マップが不正な状態になり、パニックや誤った動作を引き起こす可能性があります。

このコミットでは、builder.mkdirメソッドの冒頭でb.exec.Lock()を呼び出し、defer b.exec.Unlock()を使って関数の終了時にロックを解放するように変更されました。ここでb.execは、builder構造体内に含まれるsync.Mutexのインスタンスであると推測されます(コミットの差分からはexecフィールドの型は直接読み取れませんが、Lock()Unlock()メソッドが呼び出されていることからsync.Mutexまたは同様のインターフェースを持つ型であると判断できます)。

この変更により、builder.mkdirメソッド全体がクリティカルセクションとして保護されます。つまり、一度に1つのゴルーチンだけがこのメソッドを実行できるようになり、b.mkdirCacheへのアクセスが排他的に制御されるため、データ競合が解消されます。

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

diff --git a/src/cmd/go/build.go b/src/cmd/go/build.go
index 4a046391db..77a64f406e 100644
--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -873,6 +873,8 @@ func (b *builder) runOut(dir string, desc string, cmdargs ...interface{}) ([]byt
 
 // mkdir makes the named directory.
 func (b *builder) mkdir(dir string) error {
+\tb.exec.Lock()\n+\tdefer b.exec.Unlock()\n \t// We can be a little aggressive about being\n \t// sure directories exist.  Skip repeated calls.\n \tif b.mkdirCache[dir] {\n```

## コアとなるコードの解説

追加された2行のコードは以下の通りです。

1.  `b.exec.Lock()`:
    この行は、`builder`インスタンスの`exec`フィールド(これは`sync.Mutex`型であると推測されます)をロックします。これにより、`mkdir`メソッドのクリティカルセクションへの排他的アクセスが保証されます。もし他のゴルーチンが既にこのミューテックスをロックしている場合、現在のゴルーチンはロックが解放されるまで待機します。

2.  `defer b.exec.Unlock()`:
    この行は、`defer`キーワードを使用しています。`defer`に続くステートメントは、それを囲む関数(この場合は`mkdir`メソッド)がリターンする直前に実行されることを保証します。これにより、`mkdir`メソッドが正常に終了した場合でも、エラーが発生して途中でリターンした場合でも、確実にミューテックスがアンロックされることが保証されます。これは、ミューテックスのデッドロックを防ぐためのGoにおける一般的なイディオムです。

これらの変更により、`builder.mkdir`メソッドが並行して呼び出された場合でも、`b.mkdirCache`のような共有リソースへのアクセスが同期され、データ競合が効果的に防止されます。

## 関連リンク

*   Go Issue #2695: [https://github.com/golang/go/issues/2695](https://github.com/golang/go/issues/2695)
*   Go CL 5545052: [https://golang.org/cl/5545052](https://golang.org/cl/5545052)

## 参考にした情報源リンク

*   Go言語公式ドキュメント: `sync`パッケージ
*   Go言語公式ブログ: The Go Memory Model
*   Go言語公式ブログ: Data Races in Go
*   Go言語公式ブログ: Concurrency is not Parallelism