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

[インデックス 11415] ファイルの概要

このコミットは、Go言語の標準ライブラリである net/rpc パッケージのベンチマークテスト (server_test.go) におけるデータ競合(data race)を修正するものです。具体的には、client.Call のエラー変数 err のスコープが原因で発生していた競合状態を解消しています。

コミット

commit 290921bbb58514212f3d32a13e2de37cf4213b96
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Jan 26 20:06:27 2012 +0400

    net/rpc: fix data race in benchmark
    Fixes #2781.
    
    R=golang-dev, rsc
    CC=golang-dev, mpimenov
    https://golang.org/cl/5577053

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/290921bbb58514212f3d32a13e2de37cf4213b96

元コミット内容

net/rpc: fix data race in benchmark
Fixes #2781.

R=golang-dev, rsc
CC=golang-dev, mpimenov
https://golang.org/cl/5577053

変更の背景

このコミットは、Go言語の net/rpc パッケージのベンチマークテスト server_test.go 内で発生していたデータ競合を修正するために行われました。コミットメッセージに Fixes #2781 とあるように、これはGoのIssueトラッカーで報告された問題 #2781 に対応するものです。データ競合は、複数のゴルーチンが同時に同じメモリ領域にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生するバグの一種で、プログラムの予測不能な動作やクラッシュを引き起こす可能性があります。ベンチマークテストは通常、並行処理の性能を測定するために複数のゴルーチンを使用するため、このような競合状態が発生しやすい環境です。

前提知識の解説

データ競合 (Data Race)

データ競合とは、複数の並行実行される処理(Goにおいてはゴルーチン)が、同期メカニズムなしに同じメモリ位置にアクセスし、そのうち少なくとも1つのアクセスが書き込みである場合に発生するプログラミング上のバグです。データ競合が発生すると、プログラムの実行結果が非決定論的になり、デバッグが非常に困難になります。Go言語では、go run -race コマンドを使用することで、データ競合を検出するツール(Race Detector)を有効にできます。

net/rpc パッケージ

net/rpc はGo言語の標準ライブラリで、ネットワーク越しにリモートプロシージャコール(RPC)を行うための機能を提供します。これにより、異なるプロセスや異なるマシン上で実行されているプログラム間で、関数呼び出しのように通信を行うことができます。クライアントはサーバー上のメソッドを呼び出し、サーバーはその結果をクライアントに返します。

ゴルーチン (Goroutine)

ゴルーチンはGo言語における軽量な並行実行単位です。go キーワードの後に続く関数呼び出しによって新しいゴルーチンが起動され、その関数は他のゴルーチンと並行して実行されます。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数千、数万のゴルーチンを同時に実行することが可能です。

sync/atomic パッケージと atomic.AddInt32

sync/atomic パッケージは、低レベルのアトミック(不可分)な操作を提供します。アトミック操作は、複数のゴルーチンから同時にアクセスされても、その操作全体が中断されることなく完了することが保証されます。 atomic.AddInt32(&N, -1) は、N という int32 型の変数に -1 をアトミックに加算する操作です。これは、複数のゴルーチンが同時にカウンタを減らすようなシナリオで、正確なカウントを保証するために使用されます。

client.Call メソッド

net/rpc パッケージの Client 型が提供する Call メソッドは、リモートのRPCサーバー上のメソッドを呼び出すために使用されます。 client.Call("Arith.Add", args, reply) は、RPCサーバー上の Arith サービスに属する Add メソッドを呼び出し、args を引数として渡し、結果を reply に格納します。このメソッドはエラーを返します。

技術的詳細

問題のコードは src/pkg/net/rpc/server_test.go 内の benchmarkEndToEnd 関数にあります。このベンチマーク関数は、複数のゴルーチンを起動してRPC呼び出しを並行して実行し、その性能を測定します。

元のコードは以下のようになっていました。

func benchmarkEndToEnd(dial func() (*Client, error), b *testing.B) {
	// ...
	go func() {
		reply := new(Reply)
		for atomic.AddInt32(&N, -1) >= 0 {
			err = client.Call("Arith.Add", args, reply) // ここが問題
			if err != nil {
				b.Fatalf("rpc error: Add: expected no error but got string %q", err.Error())
			}
		}
	}()
	// ...
}

ここで問題となるのは、err 変数が go func() { ... } の外側で宣言されており、複数のゴルーチン間で共有されている点です。err = client.Call(...) の行では、client.Call が返すエラー値を共有の err 変数に代入しています。複数のゴルーチンが同時にこの行を実行すると、err 変数への書き込みが競合し、データ競合が発生します。

GoのRace Detectorは、このような共有変数への非同期な書き込みを検出します。ベンチマークテストは通常、高い並行性で実行されるため、この問題が顕在化しやすかったと考えられます。

修正は非常にシンプルで、err 変数を各ゴルーチン内でローカルに宣言するように変更することです。

コアとなるコードの変更箇所

--- a/src/pkg/net/rpc/server_test.go
+++ b/src/pkg/net/rpc/server_test.go
@@ -518,7 +518,7 @@ func benchmarkEndToEnd(dial func() (*Client, error), b *testing.B) {\
 		go func() {\
 			reply := new(Reply)\
 			for atomic.AddInt32(&N, -1) >= 0{\
-\t\t\t\terr = client.Call(\"Arith.Add\", args, reply)\
+\t\t\t\terr := client.Call(\"Arith.Add\", args, reply)\
 \t\t\t\tif err != nil {\
 \t\t\t\t\tb.Fatalf(\"rpc error: Add: expected no error but got string %q\", err.Error())\
 \t\t\t\t}\

コアとなるコードの解説

変更点は以下の1行のみです。

  • 変更前: err = client.Call("Arith.Add", args, reply)
  • 変更後: err := client.Call("Arith.Add", args, reply)

この変更により、err 変数の宣言が client.Call の呼び出しと同時に行われ、そのスコープが for ループ内の各イテレーション、またはより正確には、go func() { ... } のクロージャ内でローカルになります。

具体的には、err := ... はGoの短縮変数宣言であり、err がまだ宣言されていない場合は新しい変数を宣言し、初期値を代入します。この場合、各ゴルーチンが go func() { ... } を実行するたびに、そのゴルーチン専用の err 変数が作成されます。これにより、複数のゴルーチンがそれぞれ独立した err 変数を持つことになり、共有変数への競合する書き込みが解消され、データ競合が修正されます。

この修正は、並行処理における変数のスコープと共有の重要性を示す典型的な例です。共有されるべきでない変数をローカルスコープに限定することで、データ競合のような並行処理のバグを防ぐことができます。

関連リンク

参考にした情報源リンク