[インデックス 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の並行処理に関する彼の洞察は非常に参考になります)