[インデックス 14460] ファイルの概要
このコミットは、Go言語の標準ライブラリである net/http/httptest パッケージにおいて、historyListener 型が持つ history スライスに対する潜在的なデータ競合(race condition)を修正するものです。具体的には、sync.Mutex を導入し、history スライスへのアクセスを排他的に制御することで、複数のゴルーチンからの同時アクセスによる不正な状態変更を防ぎます。
コミット
commit d4775a7814317b20aedab0d79c7892c89a93a622
Author: Dave Cheney <dave@cheney.net>
Date: Sat Nov 24 15:50:43 2012 +1100
net/http/httptest: fix possible race on historyListener.history
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6845077
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d4775a7814317b20aedab0d79c7892c89a93a622
元コミット内容
net/http/httptest: fix possible race on historyListener.history
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6845077
変更の背景
net/http/httptest パッケージは、HTTPサーバーのテストを容易にするためのユーティリティを提供します。このパッケージ内の Server 型は、テスト中に確立されたクライアント接続を追跡するために historyListener という内部型を使用しています。historyListener は、受け入れた net.Conn オブジェクトを history という []net.Conn 型のスライスに記録します。
問題は、この history スライスへのアクセスが複数のゴルーチンから同時に行われる可能性があったことです。具体的には、Accept() メソッドで新しい接続が追加される際と、CloseClientConnections() メソッドで既存の接続がクローズされる際に、history スライスが読み書きされます。Go言語において、スライスのような共有データ構造への複数のゴルーチンからの非同期な読み書きは、データ競合(race condition)を引き起こす可能性があります。データ競合が発生すると、プログラムの動作が予測不能になったり、クラッシュしたり、不正なデータが生成されたりする可能性があります。
このコミットは、このような潜在的なデータ競合を特定し、sync.Mutex を使用して history スライスへのアクセスを同期することで、この問題を解決することを目的としています。
前提知識の解説
net/http/httptest パッケージ
net/http/httptest は、Go言語の標準ライブラリの一部で、HTTPハンドラやサーバーの単体テストを容易にするためのユーティリティを提供します。主な機能として、テスト用のHTTPサーバーを簡単に起動できる httptest.Server や、HTTPリクエストをシミュレートするための httptest.NewRequest などがあります。
net.Listener と net.Conn
net.Listener: ネットワーク接続をリッスンし、新しい接続を受け入れるためのインターフェースです。TCPサーバーなどを作成する際に使用されます。Accept()メソッドは、新しい接続が確立されるまでブロックし、その接続を表すnet.Connを返します。net.Conn: ネットワーク接続の汎用インターフェースです。読み書き操作(Read()、Write())や接続のクローズ(Close())などの基本的なネットワーク操作を提供します。
データ競合(Race Condition)
データ競合は、複数のゴルーチン(またはスレッド)が共有データに同時にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生するプログラミング上のバグです。アクセス順序が保証されないため、最終的な共有データの状態が実行ごとに異なる可能性があります。Go言語では、go run -race コマンドでデータ競合を検出できます。
sync.Mutex
sync.Mutex は、Go言語の sync パッケージで提供される相互排他ロック(mutual exclusion lock)です。共有リソースへのアクセスを同期するために使用されます。
Lock(): ミューテックスをロックします。既にロックされている場合は、ロックが解放されるまで現在のゴルーチンをブロックします。Unlock(): ミューテックスをアンロックします。ロックを保持しているゴルーチンのみがアンロックできます。
sync.Mutex を使用することで、一度に1つのゴルーチンだけが保護されたコードセクション(クリティカルセクション)を実行できるようになり、データ競合を防ぐことができます。
Goの並行処理モデル
Go言語は、CSP(Communicating Sequential Processes)に影響を受けた並行処理モデルを採用しており、ゴルーチン(軽量スレッド)とチャネル(ゴルーチン間の通信手段)を主要なプリミティブとしています。しかし、共有メモリによる並行処理も可能であり、その際には sync パッケージのプリミティブ(Mutex、WaitGroup など)を使用して同期を行う必要があります。Goの哲学は「共有メモリを通信によって共有するのではなく、通信によってメモリを共有する」ですが、既存のコードベースや特定のユースケースではミューテックスが適切な場合もあります。
技術的詳細
このコミットの核心は、historyListener 型の history スライスへのアクセスを sync.Mutex で保護することです。
変更前は、historyListener 型は history []net.Conn フィールドを持っていましたが、このスライスへのアクセスを同期するためのメカニズムがありませんでした。
type historyListener struct {
net.Listener
history []net.Conn // 保護されていない
}
これにより、以下のメソッドでデータ競合の可能性がありました。
-
Accept()メソッド: 新しい接続が受け入れられるたびに、hs.history = append(hs.history, c)によってhistoryスライスに新しいnet.Connが追加されます。複数のゴルーチンが同時にAccept()を呼び出すと、append操作が非アトミックであるため、スライスの内部構造が破損したり、一部の接続が失われたりする可能性があります。 -
CloseClientConnections()メソッド: このメソッドは、historyスライスをイテレートして、記録されているすべてのクライアント接続をクローズします。このメソッドが実行されている最中に、別のゴルーチンがAccept()を呼び出してhistoryスライスを変更しようとすると、イテレーション中にスライスが変更され、予期せぬ動作(例: パニック)を引き起こす可能性があります。
このコミットでは、これらの問題を解決するために以下の変更が行われました。
-
sync.Mutexの追加:historyListener型にsync.Mutexフィールドが追加されました。このミューテックスは、historyスライスへのアクセスを保護するために使用されます。type historyListener struct { net.Listener sync.Mutex // protects history history []net.Conn }sync.Mutexはゼロ値が有効な状態(アンロック状態)であるため、明示的な初期化は不要です。 -
Accept()メソッドでのロック:Accept()メソッド内で、historyスライスに接続を追加する前にhs.Lock()を呼び出し、追加後にhs.Unlock()を呼び出すように変更されました。これにより、append操作がアトミックに実行されることが保証されます。func (hs *historyListener) Accept() (c net.Conn, err error) { c, err = hs.Listener.Accept() if err == nil { hs.Lock() // ロック hs.history = append(hs.history, c) hs.Unlock() // アンロック } return } -
CloseClientConnections()メソッドでのロック:CloseClientConnections()メソッド内で、historyスライスをイテレートして接続をクローズする前にhl.Lock()を呼び出し、イテレーション完了後にhl.Unlock()を呼び出すように変更されました。これにより、接続クローズ処理中にhistoryスライスが他のゴルーチンによって変更されることを防ぎます。func (s *Server) CloseClientConnections() { hl, ok := s.Listener.(*historyListener) if !ok { return } hl.Lock() // ロック for _, conn := range hl.history { conn.Close() } hl.Unlock() // アンロック } -
historyListenerの初期化方法の変更:Start()およびStartTLS()メソッドでhistoryListenerを初期化する際、以前はmake([]net.Conn, 0)でhistoryスライスを明示的に初期化していましたが、ミューテックスの導入により、構造体リテラルでListenerフィールドのみを初期化するように変更されました。historyスライスはGoのゼロ値セマンティクスによりnilスライスとして初期化され、これはappend操作で問題なく扱われます。// 変更前: // s.Listener = &historyListener{s.Listener, make([]net.Conn, 0)} // 変更後: s.Listener = &historyListener{Listener: s.Listener}この変更は、ミューテックスの導入とは直接関係ありませんが、構造体の初期化をより簡潔にするためのものです。
historyスライスはnilであってもappendが正しく機能するため、明示的にmake([]net.Conn, 0)を呼び出す必要はありません。
これらの変更により、historyListener.history スライスへのすべてのアクセスが sync.Mutex によって保護され、データ競合が解消されました。
コアとなるコードの変更箇所
--- a/src/pkg/net/http/httptest/server.go
+++ b/src/pkg/net/http/httptest/server.go
@@ -36,13 +36,16 @@ type Server struct {
// accepted.
type historyListener struct {
net.Listener
- history []net.Conn
+ sync.Mutex // protects history
+ history []net.Conn
}
func (hs *historyListener) Accept() (c net.Conn, err error) {
c, err = hs.Listener.Accept()
if err == nil {
+ hs.Lock()
hs.history = append(hs.history, c)
+ hs.Unlock()
}
return
}
@@ -96,7 +99,7 @@ func (s *Server) Start() {
if s.URL != "" {
panic("Server already started")
}
- s.Listener = &historyListener{s.Listener, make([]net.Conn, 0)}
+ s.Listener = &historyListener{Listener: s.Listener}
s.URL = "http://" + s.Listener.Addr().String()
s.wrapHandler()
go s.Config.Serve(s.Listener)
@@ -122,7 +125,7 @@ func (s *Server) StartTLS() {
}
tlsListener := tls.NewListener(s.Listener, s.TLS)
- s.Listener = &historyListener{tlsListener, make([]net.Conn, 0)}
+ s.Listener = &historyListener{Listener: tlsListener}
s.URL = "https://" + s.Listener.Addr().String()
s.wrapHandler()
go s.Config.Serve(s.Listener)
@@ -161,9 +164,11 @@ func (s *Server) CloseClientConnections() {
if !ok {
return
}
+ hl.Lock()
for _, conn := range hl.history {
conn.Close()
}
+ hl.Unlock()
}
// waitGroupHandler wraps a handler, incrementing and decrementing a
コアとなるコードの解説
type historyListener struct の変更
- history []net.Conn
+ sync.Mutex // protects history
+ history []net.Conn
historyListener 構造体に sync.Mutex 型のフィールドが追加されました。このミューテックスは、その直後に定義されている history スライスへのアクセスを排他的に保護するために使用されます。コメント // protects history がその意図を明確に示しています。
func (hs *historyListener) Accept() の変更
if err == nil {
+ hs.Lock()
hs.history = append(hs.history, c)
+ hs.Unlock()
}
Accept() メソッド内で、新しい接続 c を history スライスに追加する append 操作の前後で hs.Lock() と hs.Unlock() が呼び出されています。これにより、append 操作がクリティカルセクションとなり、複数のゴルーチンが同時に history スライスを変更しようとするのを防ぎます。
func (s *Server) Start() および func (s *Server) StartTLS() の変更
- s.Listener = &historyListener{s.Listener, make([]net.Conn, 0)}
+ s.Listener = &historyListener{Listener: s.Listener}
historyListener の初期化方法が変更されました。以前は history スライスを make([]net.Conn, 0) で明示的に空のスライスとして初期化していましたが、変更後は Listener: s.Listener のように、Listener フィールドのみを初期化しています。Goのゼロ値セマンティクスにより、history スライスは自動的に nil スライスとして初期化されます。nil スライスは append 操作で問題なく機能するため、この変更はコードを簡潔にし、冗長な初期化を避けるものです。
func (s *Server) CloseClientConnections() の変更
if !ok {
return
}
+ hl.Lock()
for _, conn := range hl.history {
conn.Close()
}
+ hl.Unlock()
CloseClientConnections() メソッド内で、history スライスをイテレートして各接続をクローズするループの前後で hl.Lock() と hl.Unlock() が呼び出されています。これにより、接続をクローズしている最中に history スライスが他のゴルーチンによって変更されることを防ぎ、イテレーション中のデータ競合を回避します。
関連リンク
- Go CL 6845077: https://golang.org/cl/6845077
参考にした情報源リンク
- Go Concurrency Patterns: Mutexes: https://go.dev/blog/sync-mutex
- Go Concurrency Patterns: Pipelines and cancellation: https://go.dev/blog/pipelines (Goの並行処理全般に関する情報)
- The Go Programming Language Specification - Slice types: https://go.dev/ref/spec#Slice_types (nilスライスの挙動について)
syncpackage documentation: https://pkg.go.dev/syncnet/http/httptestpackage documentation: https://pkg.go.dev/net/http/httptest