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

[インデックス 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といった操作が行われていました。これらの操作は、複数のゴルーチンから同時に実行された場合、以下のようなデータ競合を引き起こす可能性がありました。

  1. file.nepipe++の競合:

    • ゴルーチンAがfile.nepipeの値を読み込む(例: 5)。
    • ゴルーチンBがfile.nepipeの値を読み込む(例: 5)。
    • ゴルーチンAがfile.nepipeをインクリメントし、結果を書き込む(例: 6)。
    • ゴルーチンBがfile.nepipeをインクリメントし、結果を書き込む(例: 6)。
    • 期待される値は7であるにもかかわらず、結果は6となり、インクリメントが失われる。
  2. file.nepipe = 0file.nepipe++の競合:

    • ゴルーチンAがfile.nepipeをインクリメントしようとする。
    • ゴルーチンBがfile.nepipe = 0を実行する。
    • これらの操作の順序によって、nepipeの値が意図しないものになる可能性がある。

この問題を解決するため、コミットでは以下の変更が導入されました。

  • 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 = 0atomic.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パッケージの関数がint32int64のような固定サイズの整数型を操作することを前提としているためです。この変更により、nepipeフィールドがアトミック操作の対象として適切に扱われるようになります。

src/pkg/os/file_windows.go

このファイルは、Windowsシステム固有のFile構造体の定義を含んでいます。

  • nepipeフィールドの削除:
    -	nepipe  int        // number of consecutive EPIPE in Write
    
    Windows版のfile構造体からnepipeフィールドが削除されました。これは、Windowsにおけるパイプエラーの処理メカニズムがUnix系システムとは異なり、このnepipeカウンタが不要であるためと考えられます。Windowsでは、EPIPEのような概念は直接適用されず、異なるエラー処理が行われます。

これらの変更により、Go言語のosパッケージは、並行環境下でのパイプエラー処理において、より堅牢で信頼性の高い動作を実現するようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のsync/atomicパッケージのドキュメント
  • Unix/LinuxのEPIPEエラーに関する一般的な情報
  • Go言語のデータ競合検出器に関する情報
  • Web検索結果: "Go issue 3860" (上記「Web search results for "Go issue 3860"」の内容)