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

[インデックス 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)

ゴルーチンリークは、開始されたゴルーチンが何らかの理由で終了せず、実行中の状態のまま残り続けてしまう現象です。一般的な原因としては以下のようなものがあります。

  1. チャネルからの読み込みがブロックされる: ゴルーチンがチャネルからの値の受信を待っているが、そのチャネルに誰も値を送信しない場合、ゴルーチンは永久にブロックされ、終了しません。
  2. チャネルへの書き込みがブロックされる: バッファなしチャネルや、バッファが満杯のチャネルにゴルーチンが値を送信しようとして、誰もその値を受信しない場合、ゴルーチンはブロックされます。
  3. 無限ループ: ゴルーチンが終了条件のない無限ループに入ってしまった場合。
  4. コンテキストのキャンセル忘れ: 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つの重要な変更が加えられています。

  1. donecチャネルの型変更: donecチャネルの型がchan<- bool(送信専用)からchan bool(双方向)に変更されました。これにより、ringf関数内でdonecチャネルを閉じることが可能になります。
  2. 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)