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

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

このコミットは、Go言語のランタイムにおけるガベージコレクション(GC)の挙動を修正し、特定の条件下でクロージャがGCによって回収されない問題を解決します。具体的には、ゴルーチンが終了する際に、そのゴルーチンに関連付けられた関数ポインタ(g->fnstart)をゼロ化することで、クロージャの不必要な参照を解除し、GCが正しく機能するようにします。

コミット

commit 13081942042636c6ebeee837a25977f7fdf65f1e
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon May 20 08:17:21 2013 +0400

    runtime: zeroize g->fnstart to not prevent GC of the closure
    Fixes #5493.
    
    R=golang-dev, minux.ma, iant
    CC=golang-dev
    https://golang.org/cl/9557043

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

https://github.com/golang/go/commit/13081942042636c6ebeee837a25977f7fdf65f1e

元コミット内容

Goランタイムにおいて、ゴルーチンが終了する際に、そのゴルーチンが保持している関数ポインタ g->fnstart をゼロ値(nil)に設定するように変更します。これにより、クロージャがガベージコレクタによって適切に回収されるのを妨げる可能性のある参照が解除されます。この変更は、Go issue #5493で報告された問題を修正します。

変更の背景

Go言語のガベージコレクタは、到達可能なオブジェクトを特定し、到達不能なオブジェクトを回収することでメモリを管理します。しかし、特定の状況下では、本来回収されるべきオブジェクト(特にクロージャ)が、ランタイム内部の参照によってGCから保護されてしまう問題がありました。

このコミットが修正しようとしているのは、ゴルーチン(G構造体)が終了した後も、そのゴルーチンが実行していた関数の開始アドレスを指す fnstart フィールドがクリアされずに残ってしまうケースです。もしこの fnstart がクロージャを指していた場合、ゴルーチン自体は終了していても、fnstart がそのクロージャへの参照を保持し続けるため、ガベージコレクタはそのクロージャを「到達可能」と判断し、メモリから解放しませんでした。これはメモリリークの一種であり、特に多数の短命なゴルーチンがクロージャを使用するアプリケーションでは問題となります。

Go issue #5493は、この具体的な問題が報告されたバグトラッカーのエントリです。この問題は、runtime.SetFinalizer を使用してオブジェクトのファイナライズを監視するテストケースで顕在化しました。ファイナライザが期待通りに呼び出されないということは、オブジェクトがGCによって回収されていないことを意味します。

前提知識の解説

  • Goランタイム (Go Runtime): Goプログラムの実行を管理する低レベルのシステムです。スケジューラ、ガベージコレクタ、メモリ管理などが含まれます。C言語で書かれた部分とGo言語で書かれた部分があります。
  • ゴルーチン (Goroutine): Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。各ゴルーチンは runtime.g 構造体(C言語のコードでは G 構造体として参照されることが多い)によって表現されます。
  • G 構造体: Goランタイム内部でゴルーチンを表すデータ構造です。スタックポインタ、プログラムカウンタ、現在の状態など、ゴルーチンの実行に必要な情報が含まれています。
  • g->fnstart: G 構造体内のフィールドの一つで、ゴルーチンが実行を開始した関数の開始アドレスを指すポインタです。これは、デバッグやプロファイリング、スタックトレースの生成などに利用されることがあります。
  • クロージャ (Closure): 関数が定義された環境(スコープ)にある変数を「記憶」し、その関数がその環境の外で呼び出されたときでもそれらの変数にアクセスできる機能を持つ関数です。Goでは、関数リテラルがクロージャとして振る舞うことができます。クロージャはヒープに割り当てられることが多く、ガベージコレクションの対象となります。
  • ガベージコレクション (Garbage Collection, GC): プログラムが動的に確保したメモリ領域のうち、もはや使用されない(到達不能な)ものを自動的に解放する仕組みです。GoのGCは、マーク&スイープ方式をベースとしています。GCが正しく機能しないと、メモリリークが発生し、プログラムのメモリ使用量が増加し続ける可能性があります。
  • runtime.SetFinalizer: Goの標準ライブラリ runtime パッケージが提供する関数で、特定のオブジェクトがガベージコレクタによって回収される直前に実行される関数(ファイナライザ)を設定します。これは、リソースの解放(ファイルハンドル、ネットワーク接続など)や、オブジェクトがGCされたことを検出するデバッグ目的などで使用されます。ファイナライザが呼び出されない場合、それはオブジェクトがGCされていないことを示唆します。
  • goexit0 関数: Goランタイム内部の関数で、ゴルーチンが正常に終了する際に呼び出されます。この関数は、ゴルーチンの状態を Gdead に設定し、関連するリソースをクリーンアップする役割を担います。

技術的詳細

このコミットの核心は、Goランタイムの goexit0 関数に gp->fnstart = nil; という一行を追加することです。

goexit0 関数は、ゴルーチンがその実行を完了し、終了する際に呼び出されるランタイム内部の関数です。この関数が呼び出されるということは、そのゴルーチンはもはやアクティブではなく、そのスタックや関連するリソースは解放される準備ができていることを意味します。

変更前は、goexit0 が呼び出されても gp->fnstart フィールドはクリアされませんでした。もし、このゴルーチンが実行していた関数がクロージャであり、そのクロージャがヒープ上に割り当てられていた場合、gp->fnstart はそのクロージャのコードエントリポイントを指し続けていました。ガベージコレクタは、ポインタが有効なオブジェクトを指している限り、そのオブジェクトを到達可能と判断します。したがって、たとえゴルーチン自体が終了していても、gp->fnstart がクロージャへの参照を保持しているために、そのクロージャはGCの対象から外れてしまい、メモリリークを引き起こしていました。

gp->fnstart = nil; を追加することで、ゴルーチンが終了する際に fnstart ポインタが明示的にゼロ化されます。これにより、fnstart がクロージャへの参照を保持しなくなり、そのクロージャが他のどこからも参照されていない場合、ガベージコレクタによって適切に回収されるようになります。これは、到達可能性の原則に基づいた修正であり、GCの正確性を保証するために重要です。

test/fixedbugs/issue5493.go は、この問題を再現し、修正が正しく機能することを確認するためのテストケースです。このテストでは、runtime.SetFinalizer を使用して、特定のクロージャがGCされたときにカウンタを減らすように設定しています。複数のゴルーチンでクロージャを実行し、その後GCを強制的に実行します。修正が適用されていれば、すべてのファイナライザが呼び出され、カウンタがゼロになるはずです。もし修正がなければ、カウンタはゼロにならず、テストは失敗します。

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

src/pkg/runtime/proc.c

--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1232,6 +1232,7 @@ static void
 goexit0(G *gp)
 {
 	gp->status = Gdead;
+	gp->fnstart = nil;
 	gp->m = nil;
 	gp->lockedm = nil;
 	m->curg = nil;

test/fixedbugs/issue5493.go (新規ファイル)

// run

// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"runtime"
	"sync"
	"sync/atomic"
	"time"
)

const N = 10
var count int64

func run() error {
	f1 := func() {}
	f2 := func() {
		func() {
			f1()
		}()
	}
	runtime.SetFinalizer(&f1, func(f *func()) {
		atomic.AddInt64(&count, -1)
	})
	go f2()
	return nil
}

func main() {
	count = N
	var wg sync.WaitGroup
	wg.Add(N)
	for i := 0; i < N; i++ {
		go func() {
			run()
			wg.Done()
		}()
	}
	wg.Wait()
	for i := 0; i < 2*N; i++ {
		time.Sleep(10 * time.Millisecond)
		runtime.GC()
	}
	if count != 0 {
		panic("not all finalizers are called")
	}
}

コアとなるコードの解説

src/pkg/runtime/proc.c の変更

goexit0 関数は、ゴルーチンが終了する際のクリーンアップ処理を行うGoランタイムのC言語コードです。 追加された gp->fnstart = nil; は、現在のゴルーチン gpfnstart フィールドを nil (NULLポインタ) に設定します。これにより、ゴルーチンが実行していた関数の開始アドレスへの参照が明示的に解除されます。この参照が解除されることで、もしその関数がクロージャであり、かつ他のどこからも参照されていない場合、ガベージコレクタがそのクロージャを到達不能と判断し、メモリを解放できるようになります。

test/fixedbugs/issue5493.go の解説

このテストケースは、問題の再現と修正の検証を目的としています。

  1. f1 := func() {}: 空のクロージャ f1 を定義します。このクロージャはヒープに割り当てられる可能性があります。
  2. f2 := func() { func() { f1() }() }: f2 という別のクロージャを定義します。この f2 の内部で、さらに匿名クロージャを定義し、その中で f1() を呼び出しています。この構造が、fnstart がクロージャを指し続ける原因となる特定のシナリオを模倣しています。
  3. runtime.SetFinalizer(&f1, func(f *func()) { atomic.AddInt64(&count, -1) }): f1 がガベージコレクタによって回収される直前に、グローバルカウンタ count をデクリメントするファイナライザを設定します。count は初期値 N (10) で設定されており、すべての f1 クロージャがGCされれば最終的に0になるはずです。
  4. go f2(): f2 クロージャを新しいゴルーチンとして起動します。このゴルーチンはすぐに終了します。
  5. main 関数:
    • N 個のゴルーチンを起動し、それぞれが run() 関数を呼び出します。これにより、N 個の f1 クロージャが作成され、それぞれにファイナライザが設定されます。
    • wg.Wait() で、すべての run() ゴルーチンが終了するのを待ちます。
    • for i := 0; i < 2*N; i++ { time.Sleep(10 * time.Millisecond); runtime.GC() }: 複数のGCサイクルを強制的に実行し、ファイナライザが呼び出される機会を与えます。time.Sleep は、GCが実行されるまでの間に他の処理が完了するのを待つためのものです。
    • if count != 0 { panic("not all finalizers are called") }: 最終的に count が0になっていなければ、すべてのファイナライザが呼び出されなかった(つまり、すべてのクロージャがGCされなかった)ことを意味し、テストはパニックを起こします。

このテストは、gp->fnstart = nil; の修正がなければ count が0にならずにパニックを起こし、修正があれば正常に終了することを確認します。

関連リンク

  • Go issue #5493 (元のバグ報告): 残念ながら、Goの公式GitHubリポジトリでは古いIssue番号が直接検索できない場合があります。しかし、コミットメッセージに記載されている https://golang.org/cl/9557043 は、Goのコードレビューシステム (Gerrit) のチェンジリストへのリンクであり、そこから元のIssueへの参照が見つかる可能性があります。
  • Go言語のガベージコレクションに関するドキュメントやブログ記事: GoのGCの仕組みを理解することは、この修正の重要性を理解する上で役立ちます。

参考にした情報源リンク

  • Go言語のソースコード (src/pkg/runtime/proc.c, test/fixedbugs/issue5493.go)
  • Go言語の公式ドキュメント (ガベージコレクション、ゴルーチン、クロージャに関する一般的な情報)
  • Goのコードレビューシステム (Gerrit) のチェンジリスト: https://golang.org/cl/9557043 (このリンクは、コミットメッセージに記載されているもので、元の議論や詳細な背景情報が含まれている可能性があります。)
  • Go言語のランタイムに関する技術ブログや解説記事 (Goの内部構造に関する一般的な知識)
  • Go issue #5493 (直接のリンクは見つかりませんでしたが、コミットメッセージからの情報に基づいています)
  • runtime.SetFinalizer のGoドキュメントI have provided the detailed explanation of the commit as requested. I have included all the sections specified in the "章構成" and provided a comprehensive analysis in Japanese. I also tried to find the Go issue 5493, but it seems to be an older issue that is not easily searchable on the current GitHub issue tracker. However, the commit message itself provides enough context to understand the problem it fixes.

I have outputted the explanation to standard output only, as requested.

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

このコミットは、Go言語のランタイムにおけるガベージコレクション(GC)の挙動を修正し、特定の条件下でクロージャがGCによって回収されない問題を解決します。具体的には、ゴルーチンが終了する際に、そのゴルーチンに関連付けられた関数ポインタ(`g->fnstart`)をゼロ化することで、クロージャの不必要な参照を解除し、GCが正しく機能するようにします。

## コミット

commit 13081942042636c6ebeee837a25977f7fdf65f1e Author: Dmitriy Vyukov dvyukov@google.com Date: Mon May 20 08:17:21 2013 +0400

runtime: zeroize g->fnstart to not prevent GC of the closure
Fixes #5493.

R=golang-dev, minux.ma, iant
CC=golang-dev
https://golang.org/cl/9557043

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

[https://github.com/golang/go/commit/13081942042636c6ebeee837a25977f7fdf65f1e](https://github.com/golang/go/commit/13081942042636c6ebeee837a25977f7fdf65f1e)

## 元コミット内容

Goランタイムにおいて、ゴルーチンが終了する際に、そのゴルーチンが保持している関数ポインタ `g->fnstart` をゼロ値(`nil`)に設定するように変更します。これにより、クロージャがガベージコレクタによって適切に回収されるのを妨げる可能性のある参照が解除されます。この変更は、Go issue #5493で報告された問題を修正します。

## 変更の背景

Go言語のガベージコレクタは、到達可能なオブジェクトを特定し、到達不能なオブジェクトを回収することでメモリを管理します。しかし、特定の状況下では、本来回収されるべきオブジェクト(特にクロージャ)が、ランタイム内部の参照によってGCから保護されてしまう問題がありました。

このコミットが修正しようとしているのは、ゴルーチン(`G`構造体)が終了した後も、そのゴルーチンが実行していた関数の開始アドレスを指す `fnstart` フィールドがクリアされずに残ってしまうケースです。もしこの `fnstart` がクロージャを指していた場合、ゴルーチン自体は終了していても、`fnstart` がそのクロージャへの参照を保持し続けるため、ガベージコレクタはそのクロージャを「到達可能」と判断し、メモリから解放しませんでした。これはメモリリークの一種であり、特に多数の短命なゴルーチンがクロージャを使用するアプリケーションでは問題となります。

Go issue #5493は、この具体的な問題が報告されたバグトラッカーのエントリです。この問題は、`runtime.SetFinalizer` を使用してオブジェクトのファイナライズを監視するテストケースで顕在化しました。ファイナライザが期待通りに呼び出されないということは、オブジェクトがGCによって回収されていないことを意味します。

## 前提知識の解説

*   **Goランタイム (Go Runtime)**: Goプログラムの実行を管理する低レベルのシステムです。スケジューラ、ガベージコレクタ、メモリ管理などが含まれます。C言語で書かれた部分とGo言語で書かれた部分があります。
*   **ゴルーチン (Goroutine)**: Go言語における軽量な並行実行単位です。OSのスレッドよりもはるかに軽量で、数百万のゴルーチンを同時に実行することも可能です。各ゴルーチンは `runtime.g` 構造体(C言語のコードでは `G` 構造体として参照されることが多い)によって表現されます。
*   **`G` 構造体**: Goランタイム内部でゴルーチンを表すデータ構造です。スタックポインタ、プログラムカウンタ、現在の状態など、ゴルーチンの実行に必要な情報が含まれています。
*   **`g->fnstart`**: `G` 構造体内のフィールドの一つで、ゴルーチンが実行を開始した関数の開始アドレスを指すポインタです。これは、デバッグやプロファイリング、スタックトレースの生成などに利用されることがあります。
*   **クロージャ (Closure)**: 関数が定義された環境(スコープ)にある変数を「記憶」し、その関数がその環境の外で呼び出されたときでもそれらの変数にアクセスできる機能を持つ関数です。Goでは、関数リテラルがクロージャとして振る舞うことができます。クロージャはヒープに割り当てられることが多く、ガベージコレクションの対象となります。
*   **ガベージコレクション (Garbage Collection, GC)**: プログラムが動的に確保したメモリ領域のうち、もはや使用されない(到達不能な)ものを自動的に解放する仕組みです。GoのGCは、マーク&スイープ方式をベースとしています。GCが正しく機能しないと、メモリリークが発生し、プログラムのメモリ使用量が増加し続ける可能性があります。
*   **`runtime.SetFinalizer`**: Goの標準ライブラリ `runtime` パッケージが提供する関数で、特定のオブジェクトがガベージコレクタによって回収される直前に実行される関数(ファイナライザ)を設定します。これは、リソースの解放(ファイルハンドル、ネットワーク接続など)や、オブジェクトがGCされたことを検出するデバッグ目的などで使用されます。ファイナライザが呼び出されない場合、それはオブジェクトがGCされていないことを示唆します。
*   **`goexit0` 関数**: Goランタイム内部の関数で、ゴルーチンが正常に終了する際に呼び出されます。この関数は、ゴルーチンの状態を `Gdead` に設定し、関連するリソースをクリーンアップする役割を担います。

## 技術的詳細

このコミットの核心は、Goランタイムの `goexit0` 関数に `gp->fnstart = nil;` という一行を追加することです。

`goexit0` 関数は、ゴルーチンがその実行を完了し、終了する際に呼び出されるランタイム内部の関数です。この関数が呼び出されるということは、そのゴルーチンはもはやアクティブではなく、そのスタックや関連するリソースは解放される準備ができていることを意味します。

変更前は、`goexit0` が呼び出されても `gp->fnstart` フィールドはクリアされませんでした。もし、このゴルーチンが実行していた関数がクロージャであり、そのクロージャがヒープ上に割り当てられていた場合、`gp->fnstart` はそのクロージャのコードエントリポイントを指し続けていました。ガベージコレクタは、ポインタが有効なオブジェクトを指している限り、そのオブジェクトを到達可能と判断します。したがって、たとえゴルーチン自体が終了していても、`gp->fnstart` がクロージャへの参照を保持しているために、そのクロージャはGCの対象から外れてしまい、メモリリークを引き起こしていました。

`gp->fnstart = nil;` を追加することで、ゴルーチンが終了する際に `fnstart` ポインタが明示的にゼロ化されます。これにより、`fnstart` がクロージャへの参照を保持しなくなり、そのクロージャが他のどこからも参照されていない場合、ガベージコレクタによって適切に回収されるようになります。これは、到達可能性の原則に基づいた修正であり、GCの正確性を保証するために重要です。

`test/fixedbugs/issue5493.go` は、この問題を再現し、修正が正しく機能することを確認するためのテストケースです。このテストでは、`runtime.SetFinalizer` を使用して、特定のクロージャがGCされたときにカウンタを減らすように設定しています。複数のゴルーチンでクロージャを実行し、その後GCを強制的に実行します。修正が適用されていれば、すべてのファイナライザが呼び出され、カウンタがゼロになるはずです。もし修正がなければ、カウンタはゼロにならず、テストは失敗します。

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

### `src/pkg/runtime/proc.c`

```diff
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1232,6 +1232,7 @@ static void
 goexit0(G *gp)
 {
 	gp->status = Gdead;
+	gp->fnstart = nil;
 	gp->m = nil;
 	gp->lockedm = nil;
 	m->curg = nil;

test/fixedbugs/issue5493.go (新規ファイル)

// run

// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"runtime"
	"sync"
	"sync/atomic"
	"time"
)

const N = 10
var count int64

func run() error {
	f1 := func() {}
	f2 := func() {
		func() {
			f1()
		}()
	}
	runtime.SetFinalizer(&f1, func(f *func()) {
		atomic.AddInt64(&count, -1)
	})
	go f2()
	return nil
}

func main() {
	count = N
	var wg sync.WaitGroup
	wg.Add(N)
	for i := 0; i < N; i++ {
		go func() {
			run()
			wg.Done()
		}()
	}
	wg.Wait()
	for i := 0; i < 2*N; i++ {
		time.Sleep(10 * time.Millisecond)
		runtime.GC()
	}
	if count != 0 {
		panic("not all finalizers are called")
	}
}

コアとなるコードの解説

src/pkg/runtime/proc.c の変更

goexit0 関数は、ゴルーチンが終了する際のクリーンアップ処理を行うGoランタイムのC言語コードです。 追加された gp->fnstart = nil; は、現在のゴルーチン gpfnstart フィールドを nil (NULLポインタ) に設定します。これにより、ゴルーチンが実行していた関数の開始アドレスへの参照が明示的に解除されます。この参照が解除されることで、もしその関数がクロージャであり、かつ他のどこからも参照されていない場合、ガベージコレクタがそのクロージャを到達不能と判断し、メモリを解放できるようになります。

test/fixedbugs/issue5493.go の解説

このテストケースは、問題の再現と修正の検証を目的としています。

  1. f1 := func() {}: 空のクロージャ f1 を定義します。このクロージャはヒープに割り当てられる可能性があります。
  2. f2 := func() { func() { f1() }() }: f2 という別のクロージャを定義します。この f2 の内部で、さらに匿名クロージャを定義し、その中で f1() を呼び出しています。この構造が、fnstart がクロージャを指し続ける原因となる特定のシナリオを模倣しています。
  3. runtime.SetFinalizer(&f1, func(f *func()) { atomic.AddInt64(&count, -1) }): f1 がガベージコレクタによって回収される直前に、グローバルカウンタ count をデクリメントするファイナライザを設定します。count は初期値 N (10) で設定されており、すべての f1 クロージャがGCされれば最終的に0になるはずです。
  4. go f2(): f2 クロージャを新しいゴルーチンとして起動します。このゴルーチンはすぐに終了します。
  5. main 関数:
    • N 個のゴルーチンを起動し、それぞれが run() 関数を呼び出します。これにより、N 個の f1 クロージャが作成され、それぞれにファイナライザが設定されます。
    • wg.Wait() で、すべての run() ゴルーチンが終了するのを待ちます。
    • for i := 0; i < 2*N; i++ { time.Sleep(10 * time.Millisecond); runtime.GC() }: 複数のGCサイクルを強制的に実行し、ファイナライザが呼び出される機会を与えます。time.Sleep は、GCが実行されるまでの間に他の処理が完了するのを待つためのものです。
    • if count != 0 { panic("not all finalizers are called") }: 最終的に count が0になっていなければ、すべてのファイナライザが呼び出されなかった(つまり、すべてのクロージャがGCされなかった)ことを意味し、テストはパニックを起こします。

このテストは、gp->fnstart = nil; の修正がなければ count が0にならずにパニックを起こし、修正があれば正常に終了することを確認します。

関連リンク

  • Go issue #5493 (元のバグ報告): 残念ながら、Goの公式GitHubリポジトリでは古いIssue番号が直接検索できない場合があります。しかし、コミットメッセージに記載されている https://golang.org/cl/9557043 は、Goのコードレビューシステム (Gerrit) のチェンジリストへのリンクであり、そこから元のIssueへの参照が見つかる可能性があります。
  • Go言語のガベージコレクションに関するドキュメントやブログ記事: GoのGCの仕組みを理解することは、この修正の重要性を理解する上で役立ちます。

参考にした情報源リンク

  • Go言語のソースコード (src/pkg/runtime/proc.c, test/fixedbugs/issue5493.go)
  • Go言語の公式ドキュメント (ガベージコレクション、ゴルーチン、クロージャに関する一般的な情報)
  • Goのコードレビューシステム (Gerrit) のチェンジリスト: https://golang.org/cl/9557043 (このリンクは、コミットメッセージに記載されているもので、元の議論や詳細な背景情報が含まれている可能性があります。)
  • Go言語のランタイムに関する技術ブログや解説記事 (Goの内部構造に関する一般的な知識)
  • Go issue #5493 (直接のリンクは見つかりませんでしたが、コミットメッセージからの情報に基づいています)
  • runtime.SetFinalizer のGoドキュメント