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

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

このコミットは、Go言語の標準ライブラリ sync パッケージ内の Cond 型の Wait メソッドに関するドキュメンテーションの改善を目的としています。具体的には、Cond.Wait が「偽の起床(spurious wakeup)」をしないことを明記することで、開発者の誤解を防ぎ、より正確な情報を提供します。

コミット

sync.CondWait メソッドのコメントを更新し、他のシステムとは異なり、WaitBroadcast または Signal によって起こされない限り戻らないことを明確にしています。

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

https://github.com/golang/go/commit/76eb911a3ccb386bc94f13f491ec78caa42a2bf7

元コミット内容

commit 76eb911a3ccb386bc94f13f491ec78caa42a2bf7
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Fri Feb 17 13:20:11 2012 +0400

    sync: say that Cond.Wait can not return spuriously
    
    R=golang-dev, r, rsc, remyoudompheng, r
    CC=golang-dev
    https://golang.org/cl/5674086
---
 src/pkg/sync/cond.go | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/pkg/sync/cond.go b/src/pkg/sync/cond.go
index 75494b5353..44f19fae3e 100644
--- a/src/pkg/sync/cond.go
+++ b/src/pkg/sync/cond.go
@@ -43,9 +43,10 @@ func NewCond(l Locker) *Cond {
 
 // Wait atomically unlocks c.L and suspends execution
 // of the calling goroutine.  After later resuming execution,
-// Wait locks c.L before returning.
+// Wait locks c.L before returning.  Unlike in other systems,
+// Wait cannot return unless awoken by Broadcast or Signal.
 //
-// Because L is not locked when Wait first resumes, the caller
+// Because c.L is not locked when Wait first resumes, the caller
 // typically cannot assume that the condition is true when
 // Wait returns.  Instead, the caller should Wait in a loop:
 //

変更の背景

このコミットの背景には、並行プログラミングにおける条件変数(Condition Variable)の一般的な挙動と、Go言語の sync.Cond の特定の設計があります。

多くの並行プログラミング環境(POSIXスレッド、Javaなど)における条件変数の wait 操作は、「偽の起床(spurious wakeup)」と呼ばれる現象を起こす可能性があります。これは、wait しているスレッドが、対応する signalbroadcast が呼ばれていないにもかかわらず、何らかの理由で(例えば、OSのスケジューリングの都合や、マルチプロセッサシステムでの競合状態など)待機状態から解除されてしまう現象です。このため、これらのシステムでは、wait から戻った後には必ず条件が満たされているかを再確認するループ(while ループなど)で wait を呼び出すことが推奨されています。

Go言語の sync.Cond は、このような偽の起床が発生しないように設計されています。しかし、他のシステムでの経験を持つ開発者は、Goの Cond.Wait も偽の起床を起こす可能性があると誤解するかもしれません。このコミットは、その誤解を解消し、Goの Cond.WaitBroadcast または Signal によって明示的に起こされない限り戻らないことをドキュメンテーションに明記することで、開発者が不必要なループを書くことを避け、より効率的で正確なコードを書けるようにすることを目的としています。

前提知識の解説

並行プログラミングと同期プリミティブ

並行プログラミングでは、複数のゴルーチン(またはスレッド)が同時に実行され、共有リソースにアクセスすることがあります。このとき、データ競合やデッドロックなどの問題を防ぐために、同期プリミティブ(同期メカニズム)が使用されます。

ミューテックス (Mutex)

ミューテックス(Mutual Exclusion)は、共有リソースへのアクセスを排他的に制御するための同期プリミティブです。あるゴルーチンがミューテックスをロックすると、他のゴルーチンはそのミューテックスがアンロックされるまで待機します。これにより、一度に一つのゴルーチンだけが共有リソースにアクセスすることを保証し、データ競合を防ぎます。Go言語では sync.Mutex が提供されています。

条件変数 (Condition Variable)

条件変数(Condition Variable)は、ミューテックスと組み合わせて使用される同期プリミティブです。特定の条件が満たされるまでゴルーチンを待機させ、条件が満たされたときにそのゴルーチンを起こすために使用されます。

一般的な条件変数の操作は以下の通りです。

  • Wait: ゴルーチンを待機状態にし、同時にミューテックスをアンロックします。条件が満たされて他のゴルーチンによって起こされるまで待機します。起こされた後、ミューテックスを再ロックしてから処理を続行します。
  • Signal: 待機しているゴルーチンのうち、一つだけを起こします。
  • Broadcast: 待機しているすべてのゴルーチンを起こします。

条件変数は、プロデューサー・コンシューマー問題のようなシナリオで特に有用です。例えば、コンシューマーはキューにデータが追加されるまで Wait し、プロデューサーはデータを追加した後に Signal または Broadcast を呼び出してコンシューマーを起こします。

偽の起床 (Spurious Wakeup)

前述の通り、一部のシステムでは、wait 操作が signalbroadcast が呼ばれていないにもかかわらず、待機状態から解除されることがあります。これが偽の起床です。この現象は、条件変数の実装が、OSのスケジューリングや低レベルの競合状態によって、意図しないタイミングでスレッドを再開させる可能性があるために発生します。

偽の起床に対処するため、プログラマーは wait を呼び出す際に、以下のようなループ構造を使用することが一般的なプラクティスとされています。

// 擬似コード
mutex.Lock()
for !condition_is_met {
    cond.Wait() // ここで偽の起床が発生する可能性がある
}
// 条件が満たされた後の処理
mutex.Unlock()

このループは、Wait から戻った後でも、実際に条件が満たされているかを再確認することで、偽の起床による誤動作を防ぎます。

技術的詳細

Go言語の sync.Cond は、内部的に sync.Mutex と組み合わせて使用されます。Cond のゼロ値は有効であり、NewCond 関数を使って明示的に作成することもできます。

Cond.Wait() メソッドの動作は以下の通りです。

  1. c.L (関連付けられた sync.Locker、通常は sync.Mutex) をアトミックにアンロックします。
  2. 呼び出し元のゴルーチンの実行を中断し、待機状態に入ります。
  3. c.Signal() または c.Broadcast() によって起こされた後、c.L を再ロックしてから戻ります。

このコミットが追加する重要な情報は、「Unlike in other systems, Wait cannot return unless awoken by Broadcast or Signal.」という一文です。これは、Goの sync.Cond.Wait が偽の起床を起こさないことを明確に宣言しています。

なぜGoの Cond.Wait は偽の起床を起こさないのでしょうか?これはGoランタイムのスケジューラと同期プリミティブの実装に起因します。GoのゴルーチンはOSのスレッドよりも軽量であり、Goランタイムがゴルーチンのスケジューリングをより細かく制御できます。sync.Cond の内部実装は、ゴルーチンを正確に待機させ、Signal または Broadcast が呼ばれたときにのみ起こすように設計されています。これにより、他のシステムで発生する可能性のある低レベルの競合状態やスケジューリングの不確実性による偽の起床が回避されます。

この特性は、開発者が Cond.Wait を使用する際に、不必要なループを書く必要がないことを意味します。ただし、ドキュメンテーションにも記載されているように、c.LWait から戻ったときにロックされていないため、呼び出し元は Wait が戻ったときに条件が真であると仮定することはできません。これは、Wait が戻る前に他のゴルーチンが条件を変更する可能性があるためです。したがって、条件が満たされていることを確認するためのループは依然として推奨されますが、そのループは偽の起床のためではなく、条件が他のゴルーチンによって変更された可能性に対処するためです。

つまり、Goの Cond.Wait は「偽の起床」はしないが、「条件が満たされていることの保証」はしない、という点が重要です。

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

変更は src/pkg/sync/cond.go ファイルの Wait メソッドのコメント部分です。

--- a/src/pkg/sync/cond.go
+++ b/src/pkg/sync/cond.go
@@ -43,9 +43,10 @@ func NewCond(l Locker) *Cond {
 
 // Wait atomically unlocks c.L and suspends execution
 // of the calling goroutine.  After later resuming execution,
-// Wait locks c.L before returning.
+// Wait locks c.L before returning.  Unlike in other systems,
+// Wait cannot return unless awoken by Broadcast or Signal.
 //
-// Because L is not locked when Wait first resumes, the caller
+// Because c.L is not locked when Wait first resumes, the caller
 // typically cannot assume that the condition is true when
 // Wait returns.  Instead, the caller should Wait in a loop:
 //

具体的には以下の2点が変更されています。

  1. Wait locks c.L before returning. の後に , Unlike in other systems, Wait cannot return unless awoken by Broadcast or Signal. が追加されました。
  2. Because L is not locked when Wait first resumes, the callerBecause c.L is not locked when Wait first resumes, the caller に変更され、Lc.L に修正されました。これは単なるタイポ修正です。

コアとなるコードの解説

追加されたコメントは、Goの sync.Cond.Wait の重要な特性を明確にしています。

Unlike in other systems, Wait cannot return unless awoken by Broadcast or Signal.

この一文は、Goの Cond.Wait が、他の多くの並行プログラミング環境における条件変数の wait 操作とは異なり、Broadcast または Signal メソッドが明示的に呼び出されない限り、待機状態から解除されないことを保証しています。これにより、Goの Cond.Wait は「偽の起床」を起こさないことが公式に明記されました。

この保証は、Goのランタイムがゴルーチンのスケジューリングと同期プリミティブの内部実装を完全に制御しているために可能となります。開発者は、Cond.Wait から戻った際に、それが必ず Signal または Broadcast の結果であることを信頼できます。

ただし、その後の既存のコメント Because c.L is not locked when Wait first resumes, the caller typically cannot assume that the condition is true when Wait returns. Instead, the caller should Wait in a loop: は依然として重要です。これは、Wait がミューテックスをアンロックしてから待機し、再ロックしてから戻るため、Wait が待機している間に他のゴルーチンが条件を変更する可能性があることを示しています。したがって、Wait から戻った後には、条件が本当に満たされているかを再確認するループ(for ループ)を使用することが依然として推奨されます。このループは偽の起床のためではなく、条件が他のゴルーチンによって変更された可能性に対処するためです。

まとめると、この変更はGoの Cond.Wait が偽の起床を起こさないという保証を提供し、開発者がより自信を持って sync.Cond を使用できるようにすると同時に、条件の再確認ループの必要性に関する既存のガイダンスを維持しています。

関連リンク

参考にした情報源リンク