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

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

このコミットは、Go言語の標準ライブラリsyncパッケージ内のRWMutex(読み書きミューテックス)において、誤った使用方法を検出するための変更を導入しています。具体的には、ロックされていないRWMutexをアンロックしようとした場合や、読み取りロックと書き込みロックのアンロック操作が混同された場合に、プログラムがパニックを引き起こすように修正されました。これにより、並行処理におけるデバッグが容易になり、潜在的なバグの早期発見に貢献します。

コミット

commit 22d46d53ea31b1bcee0a125f6fc1651ae2541563
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Jun 19 22:19:56 2014 -0700

    sync: detect incorrect usages of RWMutex
    Fixes #7858.
    
    LGTM=ruiu
    R=ruiu
    CC=golang-codereviews
    https://golang.org/cl/92720045

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

https://github.com/golang/go/commit/22d46d53ea31b1bcee0a125f6fc1651ae2541563

元コミット内容

sync: detect incorrect usages of RWMutex Fixes #7858.

LGTM=ruiu R=ruiu CC=golang-codereviews https://golang.org/cl/92720045

変更の背景

Go言語のsync.RWMutexは、複数のゴルーチンが共有リソースにアクセスする際の同期を管理するための重要なプリミティブです。これは、複数の読み取り操作を同時に許可しつつ、書き込み操作は排他的に行うことを可能にします。しかし、RWMutexの誤った使用、例えばロックされていないミューテックスをアンロックしようとする、または読み取りロックと書き込みロックのアンロックメソッドを誤って使用するなどのケースは、デッドロック、データ競合、あるいは予測不能なプログラムの動作を引き起こす可能性があります。

これらの誤用は、特に大規模で複雑な並行プログラムにおいて、発見が非常に困難なバグの温床となることがありました。従来のGoのRWMutexの実装では、このような不正な操作が発生しても、必ずしも即座にエラーが報告されるわけではなく、プログラムがサイレントに不正な状態に陥る可能性がありました。

このコミットは、このような一般的なRWMutexの誤用パターンを早期に検出し、開発者が問題を迅速に特定して修正できるようにするために導入されました。具体的には、不正なアンロック操作が発生した場合にプログラムがパニック(panic)を引き起こすように変更されました。これにより、開発段階で誤用が明確になり、本番環境での潜在的なバグを防ぐことができます。

コミットメッセージにはFixes #7858とありますが、このIssueの詳細は一般に公開されていないか、非常に古いものである可能性があります。しかし、コミットの目的がRWMutexの誤用検出であることは明確です。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の並行処理に関する基本的な概念とsyncパッケージの知識が必要です。

  • ミューテックス (Mutex): ミューテックスは、複数のゴルーチンが共有リソースに同時にアクセスするのを防ぐための同期プリミティブです。sync.Mutexは排他ロックを提供し、一度に一つのゴルーチンのみがロックを取得できます。これにより、データ競合を防ぎ、共有データの整合性を保ちます。

  • RWMutex (Read-Write Mutex): RWMutexは、ミューテックスの一種で、読み取り操作と書き込み操作に対して異なる排他制御を提供します。

    • 読み取りロック (Read Lock): RLock()メソッドで取得し、RUnlock()メソッドで解放します。複数のゴルーチンが同時に読み取りロックを取得できます。ただし、読み取りロックが一つでも保持されている間は、書き込みロックは取得できません。
    • 書き込みロック (Write Lock): Lock()メソッドで取得し、Unlock()メソッドで解放します。一度に一つのゴルーチンのみが書き込みロックを取得できます。書き込みロックが保持されている間は、他の読み取りロックも書き込みロックも取得できません。 RWMutexは、読み取り操作が頻繁で書き込み操作が少ない場合に、Mutexよりも高い並行性を提供できます。
  • アトミック操作 (Atomic Operations): アトミック操作は、複数のCPUコアやゴルーチンから同時にアクセスされても、その操作が中断されずに完全に実行されることを保証する操作です。Go言語ではsync/atomicパッケージを通じて提供されます。このコミットではatomic.AddInt32が使用されており、これは指定されたアドレスのint32値に指定された値をアトミックに加算し、新しい値を返す関数です。これにより、カウンタの更新などの操作がデータ競合なしに安全に行われます。

  • パニック (Panic): Goにおけるパニックは、プログラムの回復不可能なエラーを示すランタイムエラーの一種です。パニックが発生すると、現在のゴルーチンの実行が停止し、遅延関数(deferで登録された関数)が実行され、最終的にプログラムがクラッシュします(recover関数で捕捉されない限り)。このコミットでは、RWMutexの不正な使用を開発段階で明確にするために、意図的にパニックを発生させています。

  • Go Race Detector: Goには、並行処理におけるデータ競合を検出するための組み込みツールであるRace Detectorがあります。go run -racego test -raceコマンドで有効にできます。このコミットのコードにはtraceReleaseMergetraceDisabletraceEnableといったraceパッケージ関連の関数呼び出しが含まれています。これらは、パニックが発生する直前にレース検出器を一時的に無効にし、パニック処理後に再度有効にすることで、レース検出器がパニック自体をデータ競合として誤って報告するのを防ぐ、またはパニック発生時のスタックトレースをより正確にするための連携処理と考えられます。

技術的詳細

このコミットの核心は、sync.RWMutexRUnlock()メソッドとUnlock()メソッドに、不正なアンロック操作を検出するための厳密なチェックを追加した点にあります。

RWMutex.RUnlock()の変更

RWMutex.RUnlock()メソッドは、読み取りロックを解放するために使用されます。このメソッドの変更は、rw.readerCountという内部カウンタの値を監視することで、不正なRUnlock呼び出しを検出します。

  • rw.readerCountは、現在アクティブな読み取りロックの数を追跡するint32型のカウンタです。
  • rwmutexMaxReadersは、読み取りロックの最大数を示す定数で、通常は1 << 30 - 1という非常に大きな値です。これは、RWMutexが書き込みロックを取得する際に、readerCountをこの値だけデクリメントすることで、読み取りロックを一時的に「無効化」するメカニズムの一部として機能します。

変更後のRUnlock()では、atomic.AddInt32(&rw.readerCount, -1)によってreaderCountを1減らした後、その結果rが負の値になった場合に、以下の条件をチェックします。

  1. r+1 == 0: これは、readerCountが0の状態でRUnlock()が呼び出され、結果としてreaderCountが-1になったケースを検出します。つまり、ロックされていないRWMutexに対してRUnlock()が呼び出されたことを意味します。

  2. r+1 == -rwmutexMaxReaders: これは、Lock()(書き込みロックの取得)が呼び出された後にRUnlock()(読み取りロックの解放)が呼び出されたケースを検出します。Lock()readerCountrwmutexMaxReadersだけデクリメントするため、readerCountは非常に大きな負の値になります。その状態でRUnlock()が呼び出されると、readerCount-rwmutexMaxReaders - 1となり、r+1-rwmutexMaxReadersとなります。これは、書き込みロックがアクティブな状態で読み取りロックを解放しようとした不正な操作を検出します。

これらの条件のいずれかが満たされた場合、panic("sync: RUnlock of unlocked RWMutex")が呼び出され、プログラムが異常終了します。

RWMutex.Unlock()の変更

RWMutex.Unlock()メソッドは、書き込みロックを解放するために使用されます。このメソッドの変更は、rw.readerCountの値を監視することで、不正なUnlock呼び出しを検出します。

  • 書き込みロックを解放する際、r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)によってreaderCountrwmutexMaxReadersを加算します。これにより、読み取り側が再びロックを取得できるようになります。

変更後のUnlock()では、この加算後の結果rrwmutexMaxReaders以上の場合にパニックを発生させます。

  • r >= rwmutexMaxReaders: この条件は、Unlock()が呼び出された時点でrw.readerCountrwmutexMaxReadersより小さい(つまり、書き込みロックが保持されていない)場合に真となります。例えば、readerCountrwmutexMaxReadersの状態でUnlock()が呼び出されると、r2 * rwmutexMaxReadersとなり、この条件に合致します。これは、ロックされていないRWMutexに対してUnlock()が呼び出されたことを意味します。

この条件が満たされた場合、panic("sync: Unlock of unlocked RWMutex")が呼び出され、プログラムが異常終了します。

レース検出器との連携

コードにはtraceEnable()traceDisable()の呼び出しが含まれています。これらはGoのレース検出器(Race Detector)との連携を示しています。パニックが発生する直前にレース検出器を一時的に無効にし、パニック処理後に再度有効にすることで、レース検出器が誤ってパニック自体をデータ競合として報告するのを防ぐ、またはパニック発生時のスタックトレースをより正確にするための処理と考えられます。これにより、開発者は真のデータ競合とRWMutexの誤用によるパニックを明確に区別できます。

これらの厳密なチェックにより、開発者はRWMutexの誤用を早期に発見し、デバッグの労力を大幅に削減できるようになりました。

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

src/pkg/sync/rwmutex.go

RUnlock メソッドの変更:

--- a/src/pkg/sync/rwmutex.go
+++ b/src/pkg/sync/rwmutex.go
@@ -51,7 +51,11 @@ func (rw *RWMutex) RUnlock() {
 	traceReleaseMerge(unsafe.Pointer(&rw.writerSem))
 	traceDisable()
 	}
-	if atomic.AddInt32(&rw.readerCount, -1) < 0 {
+	if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {
+		if r+1 == 0 || r+1 == -rwmutexMaxReaders {
+			traceEnable()
+			panic("sync: RUnlock of unlocked RWMutex")
+		}
 		// A writer is pending.
 		if atomic.AddInt32(&rw.readerWait, -1) == 0 {
 			// The last reader unblocks the writer.

Unlock メソッドの変更:

--- a/src/pkg/sync/rwmutex.go
+++ b/src/pkg/sync/rwmutex.go
@@ -105,6 +109,10 @@ func (rw *RWMutex) Unlock() {
 
 	// Announce to readers there is no active writer.
 	r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
+	if r >= rwmutexMaxReaders {
+		traceEnable()
+		panic("sync: Unlock of unlocked RWMutex")
+	}
 	// Unblock blocked readers, if any.
 	for i := 0; i < int(r); i++ {
 		runtime_Semrelease(&rw.readerSem)

src/pkg/sync/rwmutex_test.go

追加されたテストケース:

func TestUnlockPanic(t *testing.T) {
	defer func() {
		if recover() == nil {
			t.Fatalf("unlock of unlocked RWMutex did not panic")
		}
	}()
	var mu RWMutex
	mu.Unlock()
}

func TestUnlockPanic2(t *testing.T) {
	defer func() {
		if recover() == nil {
			t.Fatalf("unlock of unlocked RWMutex did not panic")
		}
	}()
	var mu RWMutex
	mu.RLock()
	mu.Unlock()
}

func TestRUnlockPanic(t *testing.T) {
	defer func() {
		if recover() == nil {
			t.Fatalf("read unlock of unlocked RWMutex did not panic")
		}
	}()
	var mu RWMutex
	mu.RUnlock()
}

func TestRUnlockPanic2(t *testing.T) {
	defer func() {
		if recover() == nil {
			t.Fatalf("read unlock of unlocked RWMutex did not panic")
		}
	}()
	var mu RWMutex
	mu.Lock()
	mu.RUnlock()
}

コアとなるコードの解説

RWMutex.RUnlock()の変更点の詳細

RUnlock()メソッドでは、atomic.AddInt32(&rw.readerCount, -1)によってrw.readerCountをアトミックにデクリメントし、その結果をrに格納します。 その後のif r < 0という条件は、readerCountが負の値になった場合に、不正な状態であると判断するためのものです。これは、通常、読み取りロックが解放されるとreaderCountは0以上になるはずだからです。

さらに、ネストされたif文で以下の2つの具体的な不正なケースをチェックします。

  1. r+1 == 0: これは、rw.readerCountがデクリメントされる前は0であったにもかかわらずRUnlock()が呼び出されたことを意味します。つまり、ロックされていないRWMutexに対してRUnlock()が呼び出された場合にこの条件が真となり、パニックが発生します。

  2. r+1 == -rwmutexMaxReaders: これは、RWMutexが書き込みロック(Lock())によってロックされている状態でRUnlock()が呼び出された場合に発生します。Lock()メソッドは、rw.readerCountrwmutexMaxReadersだけデクリメントすることで、読み取りロックを一時的に無効化します。したがって、Lock()が呼び出された後のrw.readerCount-rwmutexMaxReadersとなります。この状態でRUnlock()が呼び出されると、rw.readerCount-rwmutexMaxReaders - 1となり、r+1-rwmutexMaxReadersとなります。これにより、書き込みロックがアクティブな状態で読み取りロックを解放しようとした不正な操作が検出され、パニックが発生します。

これらのチェックにより、RWMutexの誤用が早期に、かつ明確に報告されるようになります。

RWMutex.Unlock()の変更点の詳細

Unlock()メソッドでは、r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)によってrw.readerCountrwmutexMaxReadersを加算します。これは、書き込みロックが解放されたことを読み取り側に通知し、読み取りロックの取得を再び可能にするための操作です。

その後のif r >= rwmutexMaxReadersという条件は、不正なUnlock呼び出しを検出します。

  • 通常、Unlock()が呼び出されるのは、Lock()によって書き込みロックが取得されている状態です。このとき、rw.readerCount-rwmutexMaxReadersです。rwmutexMaxReadersを加算すると、rw.readerCount0に戻ります。
  • しかし、もしUnlock()が呼び出された時点でrw.readerCount-rwmutexMaxReadersでなかった場合(つまり、書き込みロックが保持されていない場合)、rwmutexMaxReadersを加算した結果がrwmutexMaxReaders以上になる可能性があります。例えば、rw.readerCount0の状態でUnlock()が呼び出されると、rrwmutexMaxReadersとなり、この条件に合致します。これにより、ロックされていないRWMutexに対してUnlock()が呼び出された場合にパニックが発生します。

テストケースの追加

src/pkg/sync/rwmutex_test.goに追加された4つのテスト関数は、上記のパニック検出ロジックが正しく機能することを保証します。

  • TestUnlockPanic: ロックされていないRWMutexに対してUnlock()を呼び出し、パニックが発生することを確認します。
  • TestUnlockPanic2: RLock()で読み取りロックを取得した後にUnlock()(書き込みロックの解放)を呼び出すという不正な操作を行い、パニックが発生することを確認します。
  • TestRUnlockPanic: ロックされていないRWMutexに対してRUnlock()を呼び出し、パニックが発生することを確認します。
  • TestRUnlockPanic2: Lock()で書き込みロックを取得した後にRUnlock()(読み取りロックの解放)を呼び出すという不正な操作を行い、パニックが発生することを確認します。

これらのテストは、deferrecover()のメカニズムを利用して、期待されるパニックが実際に発生したかどうかを検証しています。これにより、RWMutexの堅牢性が向上し、開発者がより安全に並行処理を記述できるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード (golang/go GitHubリポジトリ)
  • Go言語における並行処理と同期プリミティブに関する一般的な知識