[インデックス 13660] ファイルの概要
このコミットは、Go言語の標準ライブラリ os パッケージにおけるデータ競合の修正に関するものです。具体的には、Process 型の done フィールド(プロセスが正常に待機されたかどうかを示すフラグ)に対するアクセスが、複数のゴルーチンから同時に行われた場合に発生する可能性のある競合状態を解消しています。
コミット
commit 122a558f4701efc1841f7a5bc2d7c65ed4606fc1
Author: Dave Cheney <dave@cheney.net>
Date: Tue Aug 21 10:41:31 2012 +1000
os: fix data race on Process.done
Fixes #3969.
R=dvyukov, r, alex.brainman, minux.ma
CC=golang-dev
https://golang.org/cl/6462081
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/122a558f4701efc1841f7a5bc2d7c65ed4606fc1
元コミット内容
このコミットは、os パッケージ内の Process 構造体における done フィールドのデータ競合を修正します。done フィールドは、プロセスが既に終了し、その状態が待機(wait)されたかどうかを示すブール値でした。このブール値への読み書きが複数のゴルーチンから同時に行われると、予期せぬ動作やクラッシュを引き起こす可能性がありました。
修正は、done フィールドを sync/atomic パッケージの機能を使用してアトミックに操作される uint32 型の isdone フィールドに置き換えることで行われました。これにより、isdone の読み書きが不可分な操作となり、データ競合が防止されます。
変更の背景
この変更は、Go issue #3969 を修正するために行われました。Go issue #3969 は、os.Process の done フィールドがデータ競合を引き起こす可能性があることを報告しています。具体的には、Process.Wait() メソッドが done フィールドを true に設定する一方で、Process.Signal() メソッドが done フィールドを読み取る際に、同期メカニズムがないために競合が発生していました。
データ競合は、複数のゴルーチンが共有データに同時にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生します。このような状況では、操作の順序が保証されず、プログラムの動作が非決定論的になり、バグの特定が困難になります。特に、bool 型のような小さな値であっても、CPUのキャッシュやコンパイラの最適化によって、アトミックでない操作は競合状態を引き起こす可能性があります。
前提知識の解説
データ競合 (Data Race)
データ競合は、並行プログラミングにおける一般的なバグの一種です。以下の3つの条件がすべて満たされたときに発生します。
- 複数のゴルーチン(またはスレッド)が同時に同じメモリ位置にアクセスする。
- 少なくとも1つのアクセスが書き込み操作である。
- アクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。
データ競合が発生すると、プログラムの動作が予測不能になり、クラッシュ、不正な結果、デッドロックなどの問題を引き起こす可能性があります。Go言語では、go run -race コマンドで競合検出器を有効にしてプログラムを実行することで、データ競合を検出できます。
アトミック操作 (Atomic Operations)
アトミック操作とは、不可分な操作のことです。つまり、その操作が実行されている間は、他のゴルーチンから中断されることがなく、完全に実行されるか、まったく実行されないかのどちらかになります。これにより、複数のゴルーチンが同時に同じメモリ位置にアクセスしても、データ競合が発生するのを防ぐことができます。
Go言語では、sync/atomic パッケージがアトミック操作を提供します。このパッケージには、int32, int64, uint32, uint64, uintptr, Pointer などの型に対するアトミックな読み込み、書き込み、加算、比較交換(CompareAndSwap)などの関数が含まれています。これらの関数は、CPUの特別な命令を利用して、ハードウェアレベルでアトミック性を保証します。
os パッケージ
os パッケージは、オペレーティングシステムと対話するための機能を提供します。これには、ファイルシステム操作、プロセス管理、環境変数へのアクセスなどが含まれます。このコミットで修正された Process 型は、外部プロセスを表現し、そのプロセスを起動したり、シグナルを送ったり、終了を待機したりするためのメソッドを提供します。
技術的詳細
このコミットの主要な技術的変更点は、os.Process 構造体の done フィールドの扱い方です。
-
boolからuint32への型変更:- 元の
Process構造体にはdone boolフィールドがありました。ブール値は通常1バイトで表現されますが、CPUがメモリから値を読み書きする際には、ワード単位(例えば4バイトや8バイト)でアクセスすることが一般的です。アトミックでないブール値へのアクセスは、複数のゴルーチンが同時に読み書きしようとすると、部分的な書き込みや古い値の読み込みが発生する可能性があります。 - 新しい
Process構造体ではisdone uint32フィールドに変更されました。uint32は32ビットの符号なし整数であり、多くのアーキテクチャでアトミックに読み書きできる最小単位のデータ型です。sync/atomicパッケージの関数は、このuint32のような固定サイズの整数型に対してアトミック操作を提供します。
- 元の
-
sync/atomicパッケージの導入:src/pkg/os/exec.goにsync/atomicパッケージがインポートされました。Process型にsetDone()とdone()という2つの新しいメソッドが追加されました。setDone()メソッドは、atomic.StoreUint32(&p.isdone, 1)を呼び出します。これは、p.isdoneの値をアトミックに1に設定します。1はプロセスが終了した状態を表します。done()メソッドは、atomic.LoadUint32(&p.isdone) > 0を呼び出します。これは、p.isdoneの値をアトミックに読み込み、それが0より大きいかどうか(つまり1かどうか)をチェックします。
-
doneフィールドのアクセス箇所の変更:src/pkg/os/exec_plan9.go,src/pkg/os/exec_unix.go,src/pkg/os/exec_windows.goの各ファイルで、p.doneへの直接アクセスがp.done()メソッドの呼び出しに置き換えられました。- 同様に、
p.done = trueという直接的な書き込みはp.setDone()メソッドの呼び出しに置き換えられました。
これらの変更により、isdone フィールドへのすべてのアクセスが sync/atomic パッケージによって提供されるアトミック操作を介して行われるようになり、データ競合が効果的に排除されました。
コアとなるコードの変更箇所
src/pkg/os/exec.go
--- a/src/pkg/os/exec.go
+++ b/src/pkg/os/exec.go
@@ -6,6 +6,7 @@ package os
import (
"runtime"
+ "sync/atomic"
"syscall"
)
@@ -13,7 +14,7 @@ import (
type Process struct {
Pid int
handle uintptr
- done bool // process has been successfully waited on
+ isdone uint32 // process has been successfully waited on, non zero if true
}
func newProcess(pid int, handle uintptr) *Process {
@@ -22,6 +23,14 @@ func newProcess(pid int, handle uintptr) *Process {
return p
}
+func (p *Process) setDone() {
+ atomic.StoreUint32(&p.isdone, 1)
+}
+
+func (p *Process) done() bool {
+ return atomic.LoadUint32(&p.isdone) > 0
+}
+
// ProcAttr holds the attributes that will be applied to a new process
// started by StartProcess.
type ProcAttr struct {
src/pkg/os/exec_plan9.go
--- a/src/pkg/os/exec_plan9.go
+++ b/src/pkg/os/exec_plan9.go
@@ -49,7 +49,7 @@ func (p *Process) writeProcFile(file string, data string) error {
}
func (p *Process) signal(sig Signal) error {
- if p.done {
+ if p.done() {
return errors.New("os: process already finished")
}
if sig == Kill {
@@ -84,7 +84,7 @@ func (p *Process) wait() (ps *ProcessState, err error) {
}
if waitmsg.Pid == p.Pid {
- p.done = true
+ p.setDone()
break
}
}
src/pkg/os/exec_unix.go
--- a/src/pkg/os/exec_unix.go
+++ b/src/pkg/os/exec_unix.go
@@ -24,7 +24,7 @@ func (p *Process) wait() (ps *ProcessState, err error) {
return nil, NewSyscallError("wait", e)
}\n if pid1 != 0 {
- p.done = true
+ p.setDone()
}
ps = &ProcessState{
pid: pid1,
@@ -35,7 +35,7 @@ func (p *Process) wait() (ps *ProcessState, err error) {
}
func (p *Process) signal(sig Signal) error {
- if p.done {
+ if p.done() {
return errors.New("os: process already finished")
}
s, ok := sig.(syscall.Signal)
src/pkg/os/exec_windows.go
--- a/src/pkg/os/exec_windows.go
+++ b/src/pkg/os/exec_windows.go
@@ -32,7 +32,7 @@ func (p *Process) wait() (ps *ProcessState, err error) {
if e != nil {
return nil, NewSyscallError("GetProcessTimes", e)
}
- p.done = true
+ p.setDone()
// NOTE(brainman): It seems that sometimes process is not dead
// when WaitForSingleObject returns. But we do not know any
// other way to wait for it. Sleeping for a while seems to do
@@ -43,7 +43,7 @@ func (p *Process) wait() (ps *ProcessState, err error) {
}
func (p *Process) signal(sig Signal) error {
- if p.done {
+ if p.done() {
return errors.New("os: process already finished")
}
if sig == Kill {
コアとなるコードの解説
src/pkg/os/exec.go の変更点
import "sync/atomic"の追加: アトミック操作を行うためにsync/atomicパッケージがインポートされました。Process構造体の変更:done boolフィールドがisdone uint32に変更されました。これにより、アトミック操作の対象となる32ビットの符号なし整数が導入されました。コメントも「process has been successfully waited on, non zero if true」と更新され、isdoneが非ゼロの場合にプロセスが終了したことを示すようになりました。
setDone()メソッドの追加:- このメソッドは
Processポインタを受け取り、atomic.StoreUint32(&p.isdone, 1)を呼び出します。これは、p.isdoneの値をアトミックに1に設定します。これにより、複数のゴルーチンが同時にisdoneを設定しようとしても、競合が発生せず、常に正しい値が書き込まれることが保証されます。
- このメソッドは
done()メソッドの追加:- このメソッドは
Processポインタを受け取り、atomic.LoadUint32(&p.isdone) > 0を返します。これは、p.isdoneの値をアトミックに読み込み、それが0より大きいかどうか(つまり1かどうか)をチェックします。これにより、複数のゴルーチンが同時にisdoneを読み取ろうとしても、常に最新の正しい値が読み取られることが保証されます。
- このメソッドは
src/pkg/os/exec_plan9.go, src/pkg/os/exec_unix.go, src/pkg/os/exec_windows.go の変更点
これらのファイルは、各オペレーティングシステム固有の Process の実装を含んでいます。変更はすべて同じパターンに従っています。
p.doneの読み取り箇所の変更:if p.done { ... }のような条件文はif p.done() { ... }に変更されました。これにより、isdoneフィールドの読み取りがアトミックなdone()メソッドを介して行われるようになります。
p.done = trueの書き込み箇所の変更:p.done = trueのような代入文はp.setDone()に変更されました。これにより、isdoneフィールドへの書き込みがアトミックなsetDone()メソッドを介して行われるようになります。
これらの変更により、Process の状態(特に done フラグ)へのすべてのアクセスがアトミックに保護され、並行実行環境下でのデータ競合が解消されました。
関連リンク
- Go issue #3969: https://github.com/golang/go/issues/3969
- Go CL 6462081: https://golang.org/cl/6462081 (これは古いGoのコードレビューシステムへのリンクであり、現在はGitHubのコミットページにリダイレクトされるか、関連するコミットとして参照されます)
参考にした情報源リンク
- Go言語
sync/atomicパッケージのドキュメント: https://pkg.go.dev/sync/atomic - Go言語におけるデータ競合の検出: https://go.dev/doc/articles/race_detector
- Go言語
osパッケージのドキュメント: https://pkg.go.dev/os - Go言語
os/execパッケージのドキュメント: https://pkg.go.dev/os/exec - Dave Cheney のブログ記事 (Go言語の並行処理に関する一般的な情報源): https://dave.cheney.net/ (特定の記事を指すものではありませんが、Goの並行処理に関する彼の洞察は非常に参考になります)