[インデックス 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
よりも高い並行性を提供できます。
- 読み取りロック (Read Lock):
-
アトミック操作 (Atomic Operations): アトミック操作は、複数のCPUコアやゴルーチンから同時にアクセスされても、その操作が中断されずに完全に実行されることを保証する操作です。Go言語では
sync/atomic
パッケージを通じて提供されます。このコミットではatomic.AddInt32
が使用されており、これは指定されたアドレスのint32
値に指定された値をアトミックに加算し、新しい値を返す関数です。これにより、カウンタの更新などの操作がデータ競合なしに安全に行われます。 -
パニック (Panic): Goにおけるパニックは、プログラムの回復不可能なエラーを示すランタイムエラーの一種です。パニックが発生すると、現在のゴルーチンの実行が停止し、遅延関数(
defer
で登録された関数)が実行され、最終的にプログラムがクラッシュします(recover
関数で捕捉されない限り)。このコミットでは、RWMutex
の不正な使用を開発段階で明確にするために、意図的にパニックを発生させています。 -
Go Race Detector: Goには、並行処理におけるデータ競合を検出するための組み込みツールであるRace Detectorがあります。
go run -race
やgo test -race
コマンドで有効にできます。このコミットのコードにはtraceReleaseMerge
、traceDisable
、traceEnable
といったrace
パッケージ関連の関数呼び出しが含まれています。これらは、パニックが発生する直前にレース検出器を一時的に無効にし、パニック処理後に再度有効にすることで、レース検出器がパニック自体をデータ競合として誤って報告するのを防ぐ、またはパニック発生時のスタックトレースをより正確にするための連携処理と考えられます。
技術的詳細
このコミットの核心は、sync.RWMutex
のRUnlock()
メソッドと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
が負の値になった場合に、以下の条件をチェックします。
-
r+1 == 0
: これは、readerCount
が0の状態でRUnlock()
が呼び出され、結果としてreaderCount
が-1になったケースを検出します。つまり、ロックされていないRWMutex
に対してRUnlock()
が呼び出されたことを意味します。 -
r+1 == -rwmutexMaxReaders
: これは、Lock()
(書き込みロックの取得)が呼び出された後にRUnlock()
(読み取りロックの解放)が呼び出されたケースを検出します。Lock()
はreaderCount
をrwmutexMaxReaders
だけデクリメントするため、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)
によってreaderCount
にrwmutexMaxReaders
を加算します。これにより、読み取り側が再びロックを取得できるようになります。
変更後のUnlock()
では、この加算後の結果r
がrwmutexMaxReaders
以上の場合にパニックを発生させます。
r >= rwmutexMaxReaders
: この条件は、Unlock()
が呼び出された時点でrw.readerCount
がrwmutexMaxReaders
より小さい(つまり、書き込みロックが保持されていない)場合に真となります。例えば、readerCount
がrwmutexMaxReaders
の状態でUnlock()
が呼び出されると、r
は2 * 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つの具体的な不正なケースをチェックします。
-
r+1 == 0
: これは、rw.readerCount
がデクリメントされる前は0
であったにもかかわらずRUnlock()
が呼び出されたことを意味します。つまり、ロックされていないRWMutex
に対してRUnlock()
が呼び出された場合にこの条件が真となり、パニックが発生します。 -
r+1 == -rwmutexMaxReaders
: これは、RWMutex
が書き込みロック(Lock()
)によってロックされている状態でRUnlock()
が呼び出された場合に発生します。Lock()
メソッドは、rw.readerCount
をrwmutexMaxReaders
だけデクリメントすることで、読み取りロックを一時的に無効化します。したがって、Lock()
が呼び出された後のrw.readerCount
は-rwmutexMaxReaders
となります。この状態でRUnlock()
が呼び出されると、rw.readerCount
は-rwmutexMaxReaders - 1
となり、r+1
は-rwmutexMaxReaders
となります。これにより、書き込みロックがアクティブな状態で読み取りロックを解放しようとした不正な操作が検出され、パニックが発生します。
これらのチェックにより、RWMutex
の誤用が早期に、かつ明確に報告されるようになります。
RWMutex.Unlock()
の変更点の詳細
Unlock()
メソッドでは、r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)
によってrw.readerCount
にrwmutexMaxReaders
を加算します。これは、書き込みロックが解放されたことを読み取り側に通知し、読み取りロックの取得を再び可能にするための操作です。
その後のif r >= rwmutexMaxReaders
という条件は、不正なUnlock
呼び出しを検出します。
- 通常、
Unlock()
が呼び出されるのは、Lock()
によって書き込みロックが取得されている状態です。このとき、rw.readerCount
は-rwmutexMaxReaders
です。rwmutexMaxReaders
を加算すると、rw.readerCount
は0
に戻ります。 - しかし、もし
Unlock()
が呼び出された時点でrw.readerCount
が-rwmutexMaxReaders
でなかった場合(つまり、書き込みロックが保持されていない場合)、rwmutexMaxReaders
を加算した結果がrwmutexMaxReaders
以上になる可能性があります。例えば、rw.readerCount
が0
の状態でUnlock()
が呼び出されると、r
はrwmutexMaxReaders
となり、この条件に合致します。これにより、ロックされていないRWMutex
に対してUnlock()
が呼び出された場合にパニックが発生します。
テストケースの追加
src/pkg/sync/rwmutex_test.go
に追加された4つのテスト関数は、上記のパニック検出ロジックが正しく機能することを保証します。
TestUnlockPanic
: ロックされていないRWMutex
に対してUnlock()
を呼び出し、パニックが発生することを確認します。TestUnlockPanic2
:RLock()
で読み取りロックを取得した後にUnlock()
(書き込みロックの解放)を呼び出すという不正な操作を行い、パニックが発生することを確認します。TestRUnlockPanic
: ロックされていないRWMutex
に対してRUnlock()
を呼び出し、パニックが発生することを確認します。TestRUnlockPanic2
:Lock()
で書き込みロックを取得した後にRUnlock()
(読み取りロックの解放)を呼び出すという不正な操作を行い、パニックが発生することを確認します。
これらのテストは、defer
とrecover()
のメカニズムを利用して、期待されるパニックが実際に発生したかどうかを検証しています。これにより、RWMutex
の堅牢性が向上し、開発者がより安全に並行処理を記述できるようになります。
関連リンク
- Go言語の
sync
パッケージの公式ドキュメント: https://pkg.go.dev/sync - Go言語の
sync/atomic
パッケージの公式ドキュメント: https://pkg.go.dev/sync/atomic
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (golang/go GitHubリポジトリ)
- Go言語における並行処理と同期プリミティブに関する一般的な知識