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

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

このコミットは、Go言語の標準ライブラリである time パッケージに Timer.Reset メソッドを追加するものです。具体的には、以下の2つのファイルが変更されています。

  • src/pkg/time/sleep.go: Timer 型に Reset メソッドが追加され、既存の Stop メソッドのコメントが修正されています。
  • src/pkg/time/sleep_test.go: Timer.Reset メソッドの動作を検証するための新しいテストケース TestReset が追加されています。

コミット

このコミットは、Volker Doblerによって2013年1月17日に行われました。コミットの目的は、既存の Timer オブジェクトを再利用し、その有効期限を再設定するための Reset メソッドを追加することです。これにより、新しいタイマーを毎回作成するオーバーヘッドを削減し、より効率的なタイマー管理を可能にします。

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

https://github.com/golang/go/commit/44ff17e6646ae39cf6f703bb0adaa6bd21a11cf4

元コミット内容

commit 44ff17e6646ae39cf6f703bb0adaa6bd21a11cf4
Author: Volker Dobler <dr.volker.dobler@gmail.com>
Date:   Thu Jan 17 14:41:53 2013 +1100

    time: add Timer.Reset
    
    Fixes #4412.
    
    R=adg, rsc, rogpeppe, andrewdg, bradfitz
    CC=golang-dev
    https://golang.org/cl/7086050
---
 src/pkg/time/sleep.go      | 15 +++++++++++++--
 src/pkg/time/sleep_test.go | 24 ++++++++++++++++++++++++\n 2 files changed, 37 insertions(+), 2 deletions(-)

変更の背景

Go言語の time パッケージには、特定の時間が経過した後にイベントを発生させるための Timer 型が存在します。しかし、このコミット以前は、一度作成された Timer はその有効期限が切れるか、Stop() メソッドで停止されると、再利用することができませんでした。

多くのアプリケーションでは、定期的に、あるいは特定の条件が満たされた後に、同じタイマーを異なる期間で再利用したいというニーズがあります。例えば、ネットワーク接続のタイムアウト処理や、ユーザーインターフェースの要素の遅延表示などです。このようなシナリオで、毎回新しい Timer オブジェクトを作成し、ガベージコレクションの対象とすることは、パフォーマンスのオーバーヘッドやメモリ使用量の増加につながる可能性があります。

このコミットは、Go issue #4412(このコミットメッセージに記載されているイシュー番号は、現在のGitHubのイシュー番号体系とは異なる古いトラッキングシステムのものである可能性があります)で報告された、タイマーの再利用に関する要望に応えるものです。Timer.Reset メソッドの導入により、既存の Timer オブジェクトを効率的に再利用し、その有効期限を動的に変更できるようになりました。これにより、タイマーを頻繁に利用するアプリケーションのパフォーマンスとリソース効率が向上します。

前提知識の解説

Go言語の time パッケージ

Go言語の time パッケージは、時間に関する機能を提供する標準ライブラリです。時刻の表現、期間の計算、タイマー、ティックなどの機能が含まれています。

time.Duration

time.Duration は、ナノ秒単位で時間を表す型です。例えば、100 * time.Millisecond は100ミリ秒を表します。

time.Timer

time.Timer は、単一のイベントを未来の特定の時点で発生させるためのオブジェクトです。NewTimer 関数で作成され、指定された期間が経過すると、Timer オブジェクトの C チャネルに現在の時刻が送信されます。

type Timer struct {
    C <-chan Time
    // contains filtered or unexported fields
}
  • C: Timer が期限切れになったときに時刻が送信されるチャネルです。このチャネルから値を受信することで、タイマーの期限切れを検出できます。

time.NewTimer(d Duration) *Timer

指定された期間 d が経過した後にイベントを発生させる新しい Timer を作成します。

(*Timer).Stop() bool

Timer が発火するのを防ぎます。このメソッドは、タイマーが停止された場合は true を返し、既に期限切れになっているか停止されている場合は false を返します。Stop() はチャネルを閉じません。これは、チャネルからの読み取りが誤って成功するのを防ぐためです。

タイマーの内部動作(runtime.Timer

Goのタイマーは、内部的にはランタイム(runtime)パッケージの runtime.Timer 構造体と関連する関数によって管理されています。これらのタイマーは、Goランタイムによって効率的にスケジューリングされ、最小ヒープなどのデータ構造を使用して、次に発火するタイマーを効率的に特定します。

  • startTimer(t *runtime.Timer): ランタイムにタイマーを登録し、開始します。
  • stopTimer(t *runtime.Timer) bool: ランタイムからタイマーを削除し、停止します。タイマーがアクティブだった場合は true を返します。
  • nano() int64: 現在のナノ秒単位の時刻を返します。

技術的詳細

Timer.Reset メソッドは、既存の Timer オブジェクトの有効期限を新しい期間 d に変更します。その実装は以下のステップで行われます。

  1. 新しい期限の計算: when := nano() + int64(d) 現在のナノ秒時刻 (nano()) に新しい期間 d を加算することで、タイマーが次に発火する絶対時刻 when を計算します。

  2. 既存タイマーの停止: active := stopTimer(&t.r) t.rTimer 内部の runtime.Timer 構造体への参照です。stopTimer 関数を呼び出すことで、現在アクティブなタイマーをランタイムから停止・削除します。stopTimer は、タイマーが停止された(つまり、まだ発火していなかった)場合に true を返します。この戻り値は active 変数に格納され、Reset メソッドの戻り値として使用されます。

  3. 新しい期限の設定: t.r.when = when 停止したタイマーの runtime.Timer 構造体の when フィールドを、ステップ1で計算した新しい発火時刻に更新します。

  4. タイマーの再開: startTimer(&t.r) 更新された runtime.TimerstartTimer 関数に渡し、ランタイムに再登録します。これにより、タイマーは新しい期間でカウントダウンを開始します。

  5. 戻り値: return active Reset メソッドは、タイマーが Reset 呼び出し時にアクティブだった(つまり、まだ発火していなかった)場合に true を返し、既に期限切れになっていたか停止されていた場合は false を返します。この戻り値は、タイマーの状態を呼び出し元に伝えるために重要です。例えば、タイマーが既に発火していた場合、Resetfalse を返すため、呼び出し元はその事実に基づいて異なる処理を行うことができます。

この実装により、Timer オブジェクト自体を再利用しながら、その内部状態(特に発火時刻)を効率的に更新することが可能になります。

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

src/pkg/time/sleep.go

--- a/src/pkg/time/sleep.go
+++ b/src/pkg/time/sleep.go
@@ -35,10 +35,10 @@ type Timer struct {
 
 // Stop prevents the Timer from firing.
 // It returns true if the call stops the timer, false if the timer has already
-// expired or stopped.
+// expired or been stopped.
 // Stop does not close the channel, to prevent a read from the channel succeeding
 // incorrectly.
-func (t *Timer) Stop() (ok bool) {
+func (t *Timer) Stop() bool {
 	return stopTimer(&t.r)
 }
 
@@ -58,6 +58,17 @@ func NewTimer(d Duration) *Timer {
 	return t
 }
 
+// Reset changes the timer to expire after duration d.
+// It returns true if the timer had been active, false if the timer had
+// expired or been stopped.
+func (t *Timer) Reset(d Duration) bool {
+	when := nano() + int64(d)
+	active := stopTimer(&t.r)
+	t.r.when = when
+	startTimer(&t.r)
+	return active
+}
+
 func sendTime(now int64, c interface{}) {
 	// Non-blocking send of time on c.
 	// Used in NewTimer, it cannot block anyway (buffer).

src/pkg/time/sleep_test.go

--- a/src/pkg/time/sleep_test.go
+++ b/src/pkg/time/sleep_test.go
@@ -245,3 +245,27 @@ func TestSleepZeroDeadlock(t *testing.T) {\n 	}\n 	<-c\n }\n+\n+func TestReset(t *testing.T) {\n+\tt0 := NewTimer(100 * Millisecond)\n+\tSleep(50 * Millisecond)\n+\tif t0.Reset(150*Millisecond) != true {\n+\t\tt.Fatalf(\"resetting unfired timer returned false\")\n+\t}\n+\tSleep(100 * Millisecond)\n+\tselect {\n+\tcase <-t0.C:\n+\t\tt.Fatalf(\"timer fired early\")\n+\tdefault:\n+\t}\n+\tSleep(100 * Millisecond)\n+\tselect {\n+\tcase <-t0.C:\n+\tdefault:\n+\t\tt.Fatalf(\"reset timer did not fire\")\n+\t}\n+\n+\tif t0.Reset(50*Millisecond) != false {\n+\t\tt.Fatalf(\"resetting expired timer returned true\")\n+\t}\n+}\n```

## コアとなるコードの解説

### `src/pkg/time/sleep.go` の変更点

1.  **`Stop()` メソッドのコメント修正**:
    `Stop()` メソッドのコメントが `// expired or stopped.` から `// expired or been stopped.` に変更されています。これは意味的な違いはほとんどありませんが、より自然な英語表現に修正されたものです。また、`func (t *Timer) Stop() (ok bool)` から `func (t *Timer) Stop() bool` へと、戻り値の変数名 `ok` が省略されています。これはGoの慣例として、単一の戻り値でその意味が明確な場合は変数名を省略することが多いためです。

2.  **`Reset(d Duration) bool` メソッドの追加**:
    このコミットの主要な変更点です。
    - `when := nano() + int64(d)`: 新しいタイマーの発火時刻を計算します。`nano()` は現在のシステム時刻をナノ秒単位で取得します。
    - `active := stopTimer(&t.r)`: 既存のタイマーを停止し、そのタイマーが停止時にアクティブだったかどうか(つまり、まだ発火していなかったか)を示すブール値を `active` に格納します。
    - `t.r.when = when`: 内部のランタイムタイマー構造体の発火時刻を更新します。
    - `startTimer(&t.r)`: 更新されたタイマーをランタイムに再登録し、新しい期間でカウントダウンを開始させます。
    - `return active`: `Reset` メソッドは、タイマーが `Reset` 呼び出し時にアクティブだった場合に `true` を、既に期限切れか停止済みだった場合に `false` を返します。

### `src/pkg/time/sleep_test.go` の変更点

1.  **`TestReset()` 関数の追加**:
    `Timer.Reset` メソッドの動作を検証するためのテストケースです。

    - **未発火タイマーのリセット**:
        - `t0 := NewTimer(100 * Millisecond)`: 100ミリ秒後に発火するタイマーを作成。
        - `Sleep(50 * Millisecond)`: 50ミリ秒待機。タイマーはまだ発火していない状態。
        - `if t0.Reset(150*Millisecond) != true`: 未発火のタイマーを150ミリ秒にリセット。この時 `Reset` は `true` を返すはずなので、それを検証。
        - `Sleep(100 * Millisecond)`: さらに100ミリ秒待機。合計150ミリ秒経過。
        - `select { case <-t0.C: t.Fatalf("timer fired early") default: }`: タイマーがまだ発火していないことを確認。リセット後の期間は150ミリ秒なので、この時点では発火していないはず。
        - `Sleep(100 * Millisecond)`: さらに100ミリ秒待機。合計250ミリ秒経過。リセット後の期間(150ミリ秒)は既に経過している。
        - `select { case <-t0.C: default: t.Fatalf("reset timer did not fire") }`: タイマーが発火したことを確認。

    - **発火済みタイマーのリセット**:
        - `if t0.Reset(50*Millisecond) != false`: 既に発火したタイマーを50ミリ秒にリセット。この時 `Reset` は `false` を返すはずなので、それを検証。

このテストケースは、`Reset` メソッドが期待通りにタイマーの期間を変更し、適切な戻り値を返すことを確認しています。

## 関連リンク

- Go CL (Code Review) リンク: [https://golang.org/cl/7086050](https://golang.org/cl/7086050)

## 参考にした情報源リンク

- GitHubコミットページ: [https://github.com/golang/go/commit/44ff17e6646ae39cf6f703bb0adaa6bd21a11cf4](https://github.com/golang/go/commit/44ff17e6646ae39cf6f703bb0adaa6bd21a11cf4)
- Go言語 `time` パッケージ公式ドキュメント (現在のバージョン): [https://pkg.go.dev/time](https://pkg.go.dev/time) (当時のドキュメントとは異なる可能性がありますが、概念理解に役立ちます)
- Go言語のタイマーとスケジューリングに関する一般的な情報 (例: Goのランタイム内部に関する記事など)