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

[インデックス 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.Processdone フィールドがデータ競合を引き起こす可能性があることを報告しています。具体的には、Process.Wait() メソッドが done フィールドを true に設定する一方で、Process.Signal() メソッドが done フィールドを読み取る際に、同期メカニズムがないために競合が発生していました。

データ競合は、複数のゴルーチンが共有データに同時にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生します。このような状況では、操作の順序が保証されず、プログラムの動作が非決定論的になり、バグの特定が困難になります。特に、bool 型のような小さな値であっても、CPUのキャッシュやコンパイラの最適化によって、アトミックでない操作は競合状態を引き起こす可能性があります。

前提知識の解説

データ競合 (Data Race)

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

  1. 複数のゴルーチン(またはスレッド)が同時に同じメモリ位置にアクセスする。
  2. 少なくとも1つのアクセスが書き込み操作である。
  3. アクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。

データ競合が発生すると、プログラムの動作が予測不能になり、クラッシュ、不正な結果、デッドロックなどの問題を引き起こす可能性があります。Go言語では、go run -race コマンドで競合検出器を有効にしてプログラムを実行することで、データ競合を検出できます。

アトミック操作 (Atomic Operations)

アトミック操作とは、不可分な操作のことです。つまり、その操作が実行されている間は、他のゴルーチンから中断されることがなく、完全に実行されるか、まったく実行されないかのどちらかになります。これにより、複数のゴルーチンが同時に同じメモリ位置にアクセスしても、データ競合が発生するのを防ぐことができます。

Go言語では、sync/atomic パッケージがアトミック操作を提供します。このパッケージには、int32, int64, uint32, uint64, uintptr, Pointer などの型に対するアトミックな読み込み、書き込み、加算、比較交換(CompareAndSwap)などの関数が含まれています。これらの関数は、CPUの特別な命令を利用して、ハードウェアレベルでアトミック性を保証します。

os パッケージ

os パッケージは、オペレーティングシステムと対話するための機能を提供します。これには、ファイルシステム操作、プロセス管理、環境変数へのアクセスなどが含まれます。このコミットで修正された Process 型は、外部プロセスを表現し、そのプロセスを起動したり、シグナルを送ったり、終了を待機したりするためのメソッドを提供します。

技術的詳細

このコミットの主要な技術的変更点は、os.Process 構造体の done フィールドの扱い方です。

  1. bool から uint32 への型変更:

    • 元の Process 構造体には done bool フィールドがありました。ブール値は通常1バイトで表現されますが、CPUがメモリから値を読み書きする際には、ワード単位(例えば4バイトや8バイト)でアクセスすることが一般的です。アトミックでないブール値へのアクセスは、複数のゴルーチンが同時に読み書きしようとすると、部分的な書き込みや古い値の読み込みが発生する可能性があります。
    • 新しい Process 構造体では isdone uint32 フィールドに変更されました。uint32 は32ビットの符号なし整数であり、多くのアーキテクチャでアトミックに読み書きできる最小単位のデータ型です。sync/atomic パッケージの関数は、この uint32 のような固定サイズの整数型に対してアトミック操作を提供します。
  2. sync/atomic パッケージの導入:

    • src/pkg/os/exec.gosync/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 かどうか)をチェックします。
  3. 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 フラグ)へのすべてのアクセスがアトミックに保護され、並行実行環境下でのデータ競合が解消されました。

関連リンク

参考にした情報源リンク