[インデックス 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スライスの挙動について)
sync
package documentation: https://pkg.go.dev/syncnet/http/httptest
package documentation: https://pkg.go.dev/net/http/httptest