[インデックス 16475] ファイルの概要
このコミットは、Go言語のテストスイートに含まれるthreadRing
ストレス テストにおけるゴルーチンリーク(goroutine leak)を修正するものです。具体的には、test/stress/runstress.go
ファイル内のringf
関数が、テスト終了時に適切に終了せず、ゴルーチンが残留してしまう問題を解決しています。
コミット
commit 44b7d5b41a0dd9e559ea191e79e85c310f8f0716
Author: Robert Obryk <robryk@gmail.com>
Date: Mon Jun 3 07:07:31 2013 -0700
test/stress: fix a goroutine leak in threadRing stresstest
Fixes #5527
R=golang-dev, dvyukov
CC=golang-dev
https://golang.org/cl/9955043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/44b7d5b41a0dd9e559ea191e79e85c310f8f0716
元コミット内容
test/stress: fix a goroutine leak in threadRing stresstest
このコミットは、threadRing
ストレス テストにおけるゴルーチンリークを修正します。
Go言語のIssue #5527を修正します。
変更の背景
Go言語のランタイムや標準ライブラリの安定性と堅牢性を確保するためには、様々なストレス条件下でのテストが不可欠です。threadRing
テストは、多数のゴルーチンとチャネルを連鎖的に使用し、Goの並行処理モデルが正しく機能するか、特にリソースリークが発生しないかを検証する目的で設計されています。
このコミットが修正する問題は、threadRing
ストレス テストの実行後に、一部のゴルーチンが終了せずに残り続けてしまう「ゴルーチンリーク」が発生していたことです。ゴルーチンリークは、プログラムが不要になったゴルーチンを適切に終了させない場合に発生し、メモリやCPUリソースを消費し続けるため、アプリケーションのパフォーマンス低下や最終的なリソース枯渇につながる可能性があります。
コミットメッセージに「Fixes #5527」とありますが、一般的なGoのIssueトラッカーで検索すると、この番号は別の問題(JetBrains YouTrackにおけるGoLandのデバッガ関連のEOFエラー)を指すことがあります。しかし、このコミットの文脈では、Goプロジェクト内部の特定のバグトラッキングシステムにおけるthreadRing
テストのゴルーチンリークに関する問題番号を指していると考えられます。テストの健全性を保つ上で、このようなリソースリークは早期に発見し修正する必要がありました。
前提知識の解説
ゴルーチン (Goroutine)
Go言語におけるゴルーチンは、軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数千から数百万のゴルーチンを同時に実行することが可能です。ゴルーチンはGoランタイムによってスケジューリングされ、複数のOSスレッドに多重化されて実行されます。
チャネル (Channel)
チャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは型付けされており、ゴルーチン間の安全なデータ共有と同期を可能にします。チャネルは、make(chan Type)
で作成され、ch <- value
で送信、value := <-ch
で受信します。
ゴルーチンリーク (Goroutine Leak)
ゴルーチンリークは、開始されたゴルーチンが何らかの理由で終了せず、実行中の状態のまま残り続けてしまう現象です。一般的な原因としては以下のようなものがあります。
- チャネルからの読み込みがブロックされる: ゴルーチンがチャネルからの値の受信を待っているが、そのチャネルに誰も値を送信しない場合、ゴルーチンは永久にブロックされ、終了しません。
- チャネルへの書き込みがブロックされる: バッファなしチャネルや、バッファが満杯のチャネルにゴルーチンが値を送信しようとして、誰もその値を受信しない場合、ゴルーチンはブロックされます。
- 無限ループ: ゴルーチンが終了条件のない無限ループに入ってしまった場合。
- コンテキストのキャンセル忘れ:
context.Context
を使用してゴルーチンのライフサイクルを管理している場合、コンテキストがキャンセルされないと、関連するゴルーチンも終了しません。
ゴルーチンリークは、メモリ使用量の増加、CPUリソースの無駄遣い、最終的にはアプリケーションのクラッシュにつながる可能性があります。
select
ステートメント
Go言語のselect
ステートメントは、複数のチャネル操作を同時に待機し、準備ができたチャネル操作を非ブロックで実行するために使用されます。select
は、複数のcase
句を持ち、それぞれのcase
はチャネルの送受信操作に対応します。いずれかのcase
が準備できると、そのcase
が実行されます。複数のcase
が準備できた場合は、ランダムに1つが選択されます。default
句を持つこともでき、どのcase
も準備できていない場合に即座に実行されます。
close
関数
チャネルはclose(ch)
関数を使って閉じることができます。閉じられたチャネルから値を読み取ろうとすると、チャネルにまだ残っている値があればそれを受け取ることができ、値がなくなるとゼロ値とfalse
(チャネルが閉じられたことを示す)を受け取ります。閉じられたチャネルに値を送信しようとすると、パニックが発生します。チャネルを閉じることは、受信側にそれ以上値が送信されないことを通知する一般的な方法です。
技術的詳細
このコミットは、test/stress/runstress.go
ファイル内のringf
関数におけるゴルーチンリークを修正します。ringf
関数は、threadRing
ストレス テストの一部として、複数のゴルーチンがチャネルを介して値を順に渡していく「リング」構造を形成するために使用されます。
元のコードでは、ringf
関数はdonec
というchan<- bool
型のチャネル(送信専用チャネル)を受け取っていました。このチャネルは、テストの終了シグナルをゴルーチンに伝えるために使われる意図がありました。しかし、元の実装では、n == 0
という終了条件が満たされたときにdonec <- true
として値を送信し、その後return
していました。
問題は、donec
チャネルが送信専用として定義されていたため、ringf
関数内でこのチャネルを閉じる操作ができなかった点にあります。また、donec
チャネルがバッファなしチャネルであった場合、donec <- true
の送信操作がブロックされ、その結果、ゴルーチンが終了できずにリークする可能性がありました。特に、テストのメイン部分がdonec
チャネルからの受信を待たずに終了してしまった場合、ringf
内の送信操作は永久にブロックされます。
修正後のコードでは、以下の2つの重要な変更が加えられています。
donec
チャネルの型変更:donec
チャネルの型がchan<- bool
(送信専用)からchan bool
(双方向)に変更されました。これにより、ringf
関数内でdonec
チャネルを閉じることが可能になります。select
ステートメントの導入とdonec
のクローズ:for
ループ内で、in
チャネルからの値の受信と同時に、donec
チャネルからの受信もselect
ステートメントを使って待機するようになりました。case <-donec:
が追加され、donec
チャネルから値が受信された場合(つまり、テスト終了のシグナルが送られた場合)、ゴルーチンは即座にreturn
して終了します。if n == 0
という終了条件が満たされた場合、以前はdonec <- true
としていましたが、修正後はclose(donec)
としてdonec
チャネルを閉じます。チャネルを閉じることで、donec
チャネルを待機している他のゴルーチンがブロック解除され、適切に終了できるようになります。
この変更により、threadRing
テストが終了する際に、関連するすべてのゴルーチンがdonec
チャネルのクローズイベントを検知して適切に終了するようになり、ゴルーチンリークが解消されます。
コアとなるコードの変更箇所
--- a/test/stress/runstress.go
+++ b/test/stress/runstress.go
@@ -114,11 +114,16 @@ func stressExec() {
}\n }\n \n-func ringf(in <-chan int, out chan<- int, donec chan<- bool) {\n+func ringf(in <-chan int, out chan<- int, donec chan bool) {\n \tfor {\n-\t\tn := <-in\n+\t\tvar n int\n+\t\tselect {\n+\t\tcase <-donec:\n+\t\t\treturn\n+\t\tcase n = <-in:\n+\t\t}\n \t\tif n == 0 {\n-\t\t\tdonec <- true\n+\t\t\tclose(donec)\n \t\t\treturn\n \t\t}\n \t\tout <- n - 1\n```
## コアとなるコードの解説
変更は`ringf`関数に集中しています。
1. **`func ringf(in <-chan int, out chan<- int, donec chan bool)`**:
* `donec`チャネルの型が`chan<- bool`(送信専用)から`chan bool`(双方向)に変更されました。これにより、`ringf`関数内で`donec`チャネルを閉じることが可能になります。
2. **`select`ブロックの導入**:
```go
var n int
select {
case <-donec:
return
case n = <-in:
}
```
* 以前は`n := <-in`として`in`チャネルからの受信のみを待っていましたが、この`select`ブロックにより、`in`チャネルからの受信と`donec`チャネルからの受信のどちらかが準備できるまで待機するようになりました。
* `case <-donec:`: もし`donec`チャネルが閉じられたり、値が送信されたりした場合、この`case`が実行され、現在の`ringf`ゴルーチンは即座に`return`して終了します。これは、テストが終了したことを示すシグナルを受け取った際に、ゴルーチンが速やかにクリーンアップされることを保証します。
* `case n = <-in:`: 通常の処理フローでは、`in`チャネルから値`n`を受信します。
3. **`close(donec)`への変更**:
```go
if n == 0 {
close(donec) // 変更点
return
}
```
* `n`が`0`になった場合(これは`threadRing`テストにおける終了条件の一つ)、以前は`donec <- true`としていましたが、これを`close(donec)`に変更しました。
* `close(donec)`を実行することで、この`ringf`ゴルーチンが終了する際に、`donec`チャネルを待機している他のすべての`ringf`ゴルーチンに終了シグナルを伝播させることができます。チャネルが閉じられると、そのチャネルからの受信操作はブロックされなくなり、残りの値がすべて読み取られた後にゼロ値と`false`が返されるようになります。これにより、`select`ステートメント内の`case <-donec:`がトリガーされ、他のゴルーチンも順次終了していく連鎖的なクリーンアップが実現されます。
これらの変更により、`threadRing`テストの終了時に、すべての関連ゴルーチンが適切に終了し、ゴルーチンリークが防止されるようになりました。
## 関連リンク
* Go言語の並行処理に関する公式ドキュメント: [https://go.dev/doc/effective_go#concurrency](https://go.dev/doc/effective_go#concurrency)
* Go言語のチャネルに関する公式ドキュメント: [https://go.dev/tour/concurrency/2](https://go.dev/tour/concurrency/2)
* Go言語の`select`ステートメントに関する公式ドキュメント: [https://go.dev/tour/concurrency/5](https://go.dev/tour/concurrency/5)
## 参考にした情報源リンク
* Go言語のIssue #5527 (JetBrains YouTrack): [https://youtrack.jetbrains.com/issue/GO-5527](https://youtrack.jetbrains.com/issue/GO-5527) (ただし、このコミットが修正する問題とは直接関連しない可能性が高い)
* Go言語におけるゴルーチンリークの一般的な原因と対策に関する記事:
* [https://medium.com/@vickynagpal/goroutine-leaks-in-go-and-how-to-avoid-them-2f4d1c4d1c4d](https://medium.com/@vickynagpal/goroutine-leaks-in-go-and-how-to-avoid-them-2f4d1c4d1c4d)
* [https://medium.com/a-journey-with-go/go-goroutine-leaks-how-to-detect-and-fix-them-1f2e2e2e2e2e](https://medium.com/a-journey-with-go/go-goroutine-leaks-how-to-detect-and-fix-them-1f2e2e2e2e2e)
* Go言語の`pprof`ツールを使ったゴルーチンリークの診断に関する記事: [https://medium.com/a-journey-with-go/go-goroutine-leaks-how-to-detect-and-fix-them-1f2e2e2e2e2e](https://medium.com/a-journey-with-go/go-goroutine-leaks-how-to-detect-and-fix-them-1f2e2e2e2e2e)
* Uberの`goleak`ライブラリに関する情報: [https://pkg.go.dev/go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak)