[インデックス 13509] ファイルの概要
このコミットは、Go言語の標準ライブラリos
パッケージにおけるepipecheck()
関数内のデータ競合(data race)を修正するものです。具体的には、File
構造体のnepipe
フィールドへのアクセスが複数のゴルーチンから同時に行われる可能性があり、これにより競合状態が発生していました。この修正は、sync/atomic
パッケージの関数を使用してnepipe
フィールドへのアクセスをアトミック(不可分)にすることで、このデータ競合を解消しています。
コミット
commit ab9ccedefe94600b856ed5afbf6bb5849daa9b14
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Fri Jul 27 15:05:13 2012 +0400
os: fix data race in epipecheck()
Fixes #3860.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6443051
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ab9ccedefe94600b856ed5afbf6bb5849daa9b14
元コミット内容
os: fix data race in epipecheck()
Fixes #3860.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6443051
変更の背景
この変更は、Go言語のos
パッケージ内のepipecheck()
関数で発生していたデータ競合を修正するために行われました。データ競合は、複数のゴルーチンが同時に共有データにアクセスし、少なくとも1つのアクセスが書き込みである場合に発生するプログラミング上のバグです。このような競合は、プログラムの予測不可能な動作やクラッシュを引き起こす可能性があります。
具体的には、epipecheck()
関数は、パイプへの書き込み中にsyscall.EPIPE
エラー(Broken pipeエラー)が発生した回数をFile
構造体のnepipe
フィールドでカウントしていました。このnepipe
フィールドは、複数のゴルーチンから同時に読み書きされる可能性があり、その結果としてデータ競合が発生していました。Issue #3860として報告されたこの問題は、特に並行処理が多い環境で顕在化し、プログラムの安定性に影響を与えていました。
このコミットの目的は、nepipe
フィールドへのアクセスをアトミック操作に置き換えることで、このデータ競合を安全に解消し、os
パッケージの堅牢性を向上させることでした。
前提知識の解説
データ競合 (Data Race)
データ競合とは、複数の並行実行される処理(Go言語ではゴルーチン)が、同期メカニズムなしに同じメモリ位置にアクセスし、そのうち少なくとも1つが書き込み操作である場合に発生する競合状態の一種です。データ競合が発生すると、プログラムの実行結果がアクセス順序に依存するようになり、予測不能な動作、不正な値、クラッシュなどを引き起こす可能性があります。Go言語では、go run -race
コマンドでデータ競合検出器を有効にしてプログラムを実行することで、データ競合を検出できます。
sync/atomic
パッケージ
sync/atomic
パッケージは、Go言語で低レベルのアトミック操作を提供する標準ライブラリです。アトミック操作とは、その操作が中断されることなく、単一の不可分なステップとして実行されることを保証する操作です。これにより、複数のゴルーチンが同時に共有変数にアクセスしても、データ競合が発生することなく安全に操作できます。
このパッケージは、主に以下の種類のアトミック操作を提供します。
- Add: 指定された値にアトミックに加算します。
- CompareAndSwap (CAS): 変数の現在の値が期待値と一致する場合にのみ、新しい値にアトミックに更新します。
- Load: 変数の値をアトミックに読み込みます。
- Store: 変数に値をアトミックに書き込みます。
これらの操作は、ミューテックス(sync.Mutex
)のような高レベルの同期プリミティブを使用するよりも、特定のシナリオでより効率的な並行処理を実現できます。
syscall.EPIPE
syscall.EPIPE
は、Unix系システムにおけるエラーコードの一つで、「Broken pipe」(壊れたパイプ)を意味します。これは、プロセスがパイプやソケットに書き込もうとした際に、その読み込み側がすでに閉じられている場合に発生します。例えば、cat
コマンドの出力をhead
コマンドにパイプで渡し、head
が読み込みを終えて終了した後にcat
がさらに書き込もうとすると、EPIPE
エラーが発生します。Go言語のos
パッケージでは、ファイルへの書き込み操作中にこのエラーが発生することがあります。
sigpipe()
sigpipe()
は、Goランタイム内部で定義されている関数で、EPIPE
エラーが連続して発生した場合に呼び出されます。これは、通常、SIGPIPE
シグナルを発生させることで、プログラムがパイプの破損を適切に処理できるようにするためのメカニズムです。SIGPIPE
シグナルは、デフォルトではプロセスを終了させますが、Goプログラムではシグナルハンドラによって捕捉され、より優雅なエラー処理が行われることがあります。
技術的詳細
このコミットの技術的な核心は、os
パッケージ内のFile
構造体のnepipe
フィールドへのアクセスを、通常の非アトミックな操作からsync/atomic
パッケージを用いたアトミック操作へと変更した点にあります。
変更前は、epipecheck
関数内でfile.nepipe++
やfile.nepipe = 0
といった操作が行われていました。これらの操作は、複数のゴルーチンから同時に実行された場合、以下のようなデータ競合を引き起こす可能性がありました。
-
file.nepipe++
の競合:- ゴルーチンAが
file.nepipe
の値を読み込む(例: 5)。 - ゴルーチンBが
file.nepipe
の値を読み込む(例: 5)。 - ゴルーチンAが
file.nepipe
をインクリメントし、結果を書き込む(例: 6)。 - ゴルーチンBが
file.nepipe
をインクリメントし、結果を書き込む(例: 6)。 - 期待される値は7であるにもかかわらず、結果は6となり、インクリメントが失われる。
- ゴルーチンAが
-
file.nepipe = 0
とfile.nepipe++
の競合:- ゴルーチンAが
file.nepipe
をインクリメントしようとする。 - ゴルーチンBが
file.nepipe = 0
を実行する。 - これらの操作の順序によって、
nepipe
の値が意図しないものになる可能性がある。
- ゴルーチンAが
この問題を解決するため、コミットでは以下の変更が導入されました。
sync/atomic
パッケージのインポート:src/pkg/os/file_posix.go
に"sync/atomic"
が追加されました。file.nepipe
の型変更:src/pkg/os/file_unix.go
では、nepipe
フィールドの型がint
からint32
に変更されました。これは、sync/atomic
パッケージの関数が特定の固定サイズの整数型(int32
,int64
など)を操作するためです。Windows版のfile_windows.go
からはnepipe
フィールドが削除されていますが、これはWindowsではEPIPE
エラーの処理がUnix系システムとは異なるため、このフィールドが不要になったためと考えられます。- アトミックなインクリメント:
file.nepipe++
はatomic.AddInt32(&file.nepipe, 1)
に置き換えられました。atomic.AddInt32
は、指定されたポインタが指すint32
型の値に、指定されたデルタ値(ここでは1)をアトミックに加算し、その結果の新しい値を返します。これにより、複数のゴルーチンが同時にインクリメントを試みても、競合することなく正確にカウントアップされます。 - アトミックなストア(書き込み):
file.nepipe = 0
はatomic.StoreInt32(&file.nepipe, 0)
に置き換えられました。atomic.StoreInt32
は、指定されたポインタが指すint32
型の変数に、指定された値をアトミックに書き込みます。これにより、nepipe
の値を0にリセットする操作も安全に行われます。
これらの変更により、epipecheck()
関数内のnepipe
フィールドへのすべてのアクセスがアトミックになり、データ競合が完全に解消されました。これにより、Goプログラムがパイプ関連のエラーをより堅牢に処理できるようになりました。
コアとなるコードの変更箇所
--- a/src/pkg/os/file_posix.go
+++ b/src/pkg/os/file_posix.go
@@ -7,6 +7,7 @@
package os
import (
+ "sync/atomic"
"syscall"
"time"
)
@@ -15,12 +16,11 @@ func sigpipe() // implemented in package runtime
func epipecheck(file *File, e error) {
if e == syscall.EPIPE {
- file.nepipe++
- if file.nepipe >= 10 {
+ if atomic.AddInt32(&file.nepipe, 1) >= 10 {
sigpipe()
}
} else {
- file.nepipe = 0
+ atomic.StoreInt32(&file.nepipe, 0)
}
}
diff --git a/src/pkg/os/file_unix.go b/src/pkg/os/file_unix.go
index 6271c3189e..12daa70a76 100644
--- a/src/pkg/os/file_unix.go
+++ b/src/pkg/os/file_unix.go
@@ -24,7 +24,7 @@ type file struct {
fd int
name string
dirinfo *dirInfo // nil unless directory being read
- nepipe int // number of consecutive EPIPE in Write
+ nepipe int32 // number of consecutive EPIPE in Write
}
// Fd returns the integer Unix file descriptor referencing the open file.
diff --git a/src/pkg/os/file_windows.go b/src/pkg/os/file_windows.go
index 88fa77bb84..320ee22518 100644
--- a/src/pkg/os/file_windows.go
+++ b/src/pkg/os/file_windows.go
@@ -25,7 +25,6 @@ type file struct {
fd syscall.Handle
name string
dirinfo *dirInfo // nil unless directory being read
- nepipe int // number of consecutive EPIPE in Write
l sync.Mutex // used to implement windows pread/pwrite
}
コアとなるコードの解説
src/pkg/os/file_posix.go
このファイルは、POSIX互換システム(Unix, Linux, macOSなど)におけるファイル操作の共通ロジックを定義しています。
-
import "sync/atomic"
の追加:+ "sync/atomic"
sync/atomic
パッケージが新しくインポートされました。これにより、アトミック操作のための関数(AddInt32
,StoreInt32
など)が利用可能になります。 -
epipecheck
関数の変更:- file.nepipe++ - if file.nepipe >= 10 { + if atomic.AddInt32(&file.nepipe, 1) >= 10 {
file.nepipe
のインクリメント操作が、非アトミックなfile.nepipe++
からatomic.AddInt32(&file.nepipe, 1)
に置き換えられました。&file.nepipe
:nepipe
フィールドのアドレスを渡します。atomic
パッケージの関数はポインタを受け取ります。1
:nepipe
に加算する値です。atomic.AddInt32
は、加算後の新しい値を返します。この返り値が10
以上であるかをチェックすることで、連続するEPIPE
エラーの回数を安全に判定します。
- file.nepipe = 0 + atomic.StoreInt32(&file.nepipe, 0)
file.nepipe
を0にリセットする操作が、非アトミックなfile.nepipe = 0
からatomic.StoreInt32(&file.nepipe, 0)
に置き換えられました。&file.nepipe
:nepipe
フィールドのアドレスを渡します。0
:nepipe
に書き込む値です。atomic.StoreInt32
は、指定された値をアトミックに変数に書き込みます。
src/pkg/os/file_unix.go
このファイルは、Unix系システム固有のFile
構造体の定義を含んでいます。
nepipe
フィールドの型変更:- nepipe int // number of consecutive EPIPE in Write + nepipe int32 // number of consecutive EPIPE in Write
nepipe
フィールドの型がint
からint32
に変更されました。これは、sync/atomic
パッケージの関数がint32
やint64
のような固定サイズの整数型を操作することを前提としているためです。この変更により、nepipe
フィールドがアトミック操作の対象として適切に扱われるようになります。
src/pkg/os/file_windows.go
このファイルは、Windowsシステム固有のFile
構造体の定義を含んでいます。
nepipe
フィールドの削除:
Windows版の- nepipe int // number of consecutive EPIPE in Write
file
構造体からnepipe
フィールドが削除されました。これは、Windowsにおけるパイプエラーの処理メカニズムがUnix系システムとは異なり、このnepipe
カウンタが不要であるためと考えられます。Windowsでは、EPIPE
のような概念は直接適用されず、異なるエラー処理が行われます。
これらの変更により、Go言語のos
パッケージは、並行環境下でのパイプエラー処理において、より堅牢で信頼性の高い動作を実現するようになりました。
関連リンク
- GitHub Issue #3860: https://github.com/golang/go/issues/3860
- Go CL 6443051: https://golang.org/cl/6443051
参考にした情報源リンク
- Go言語の
sync/atomic
パッケージのドキュメント - Unix/Linuxの
EPIPE
エラーに関する一般的な情報 - Go言語のデータ競合検出器に関する情報
- Web検索結果: "Go issue 3860" (上記「Web search results for "Go issue 3860"」の内容)