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

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

このコミットは、Go言語の標準ライブラリであるtimeパッケージにtime.Tick()関数を追加するものです。time.Tick()は、指定された時間間隔で定期的にイベントを発生させるためのメカニズムを提供し、Goにおける時間ベースの処理やスケジューリングの基本的な構成要素となります。

コミット

commit c7bab46d0f3d4ddf13522470d49ed7d69642760c
Author: Russ Cox <rsc@golang.org>
Date:   Wed Dec 3 16:40:00 2008 -0800

    add time.Tick()
    
    R=r
    DELTA=130  (115 added, 1 deleted, 14 changed)
    OCL=20376
    CL=20385

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

https://github.com/golang/go/commit/c7bab46d0f3d4ddf13522470d49ed7d69642760c

元コミット内容

add time.Tick()

このコミットは、Go言語のtimeパッケージにTick()関数を追加します。

変更の背景

このコミットが行われた2008年12月は、Go言語がまだ公開される前の初期開発段階でした。当時のGoは、並行処理を言語レベルでサポートする新しいプログラミングパラダイムを模索しており、その中で時間ベースのイベント処理は重要な要素でした。

time.Tick()のような機能は、以下のようなシナリオで不可欠です。

  • 定期的なタスクの実行: 例えば、ログのフラッシュ、メトリクスの収集、キャッシュの更新など、一定間隔で実行する必要がある処理。
  • タイムアウト処理: 特定の操作が完了するまでの最大時間を設定し、その時間を超えた場合に処理を中断する。
  • アニメーションやゲームループ: 一定のフレームレートで画面を更新したり、ゲームの状態を進行させたりする。
  • ポーリング: 外部リソースの状態を定期的にチェックする。

この機能の導入は、Goの並行処理モデル(goroutineとchannel)を活用し、シンプルかつ効率的な方法でこれらの時間ベースの処理を実現するための初期ステップでした。コミットメッセージのコメントにあるように、この初期実装は「シンプルなプレースホルダー」であり、将来的にはより洗練された「単一の中央時間サーバー」や「tickerをキャンセルする方法」が必要であると認識されていました。これは、Go言語の設計が初期段階から将来の拡張性や効率性を考慮していたことを示しています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の基本的な概念と、関連するシステムプログラミングの知識が必要です。

Go言語の並行処理 (GoroutineとChannel)

  • Goroutine: Go言語における軽量なスレッドのようなものです。数千、数万といった多数のgoroutineを同時に実行してもオーバーヘッドが非常に小さいのが特徴です。goキーワードを使って関数を呼び出すことで、新しいgoroutineが起動されます。
  • Channel: goroutine間でデータを安全にやり取りするための通信メカニズムです。チャネルは、データの送信と受信を同期させることで、競合状態(race condition)を防ぎます。make(chan Type)で作成し、ch <- valueで送信、value := <-chで受信します。

time.Tick()は、内部でgoroutineを起動し、channelを通じて時間イベントを通知します。

システムコール (Syscall)

システムコールは、ユーザープログラムがオペレーティングシステム(OS)のカーネル機能にアクセスするためのインターフェースです。ファイルI/O、ネットワーク通信、メモリ管理、プロセス制御など、OSが提供する低レベルな機能を利用する際に使用されます。

このコミットでは、特に以下のシステムコールに関連する概念が重要です。

  • select()システムコール: Unix系OSで提供されるI/O多重化(I/O multiplexing)のためのシステムコールです。複数のファイルディスクリプタ(ソケット、パイプなど)の状態を監視し、いずれかが読み書き可能になったり、エラーが発生したりするのを待ちます。また、タイムアウト値を指定することで、指定時間内にイベントが発生しなかった場合に処理を続行することも可能です。time.Tick()の初期実装では、このselect()システムコールをタイムアウト目的で利用しています。
  • timeval構造体: select()システムコールなどで使用される時間値を表現するための構造体です。通常、秒(tv_sec)とマイクロ秒(tv_usec)の2つのフィールドを持ちます。

ナノ秒 (Nanoseconds)

時間は非常に細かい粒度で計測されることが多く、特にシステムプログラミングやパフォーマンスが重要なアプリケーションではナノ秒(10億分の1秒)単位での精度が求められます。Goのtimeパッケージは、ナノ秒単位での時間計測をサポートしています。

技術的詳細

このコミットで導入されたtime.Tick()の初期実装は、Goの並行処理プリミティブとOSの低レベルな時間管理機能を組み合わせています。

  1. nstotimeval関数の追加:

    • src/lib/syscall/time_amd64_darwin.gosrc/lib/syscall/time_amd64_linux.gonstotimeval関数が追加されました。
    • この関数は、ナノ秒単位の時間(ns)を受け取り、それをTimeval構造体(秒とマイクロ秒)に変換します。
    • ns += 999;という行は、ナノ秒をマイクロ秒に変換する際に、切り上げを行うためのものです。例えば、1000ナノ秒未満の値をマイクロ秒に変換する際に、0にならないように調整しています。
    • tv.sec = int64(ns/1000000000); で秒を計算し、tv.usec = uint32(ns%1000000000 / 1000); でマイクロ秒を計算しています。
  2. time/tick.goの新規追加:

    • このファイルはtime.Tick()の主要なロジックを含んでいます。
    • Ticker(ns int64, c *chan int64)関数が定義されています。
      • この関数は無限ループで動作し、指定されたns(ナノ秒)間隔でチャネルcに現在のナノ秒タイムスタンプを送信します。
      • ループ内でsyscall.nstotimeval(ns, &tv);を呼び出し、待機時間をTimeval構造体に変換します。
      • syscall.Syscall6(syscall.SYS_SELECT, 0, 0, 0, 0, syscall.TimevalPtr(&tv), 0);という行が重要です。これはselect()システムコールを呼び出しています。
        • SYS_SELECTselect()システムコールに対応する定数です。
        • 引数の0, 0, 0, 0は、監視するファイルディスクリプタセット(読み込み、書き込み、例外)が空であることを示します。つまり、このselect()呼び出しはI/Oイベントを待つのではなく、純粋にタイムアウト機能として利用されています。
        • syscall.TimevalPtr(&tv)は、タイムアウト時間を指定するためのポインタです。
      • select()がタイムアウトすると、time.Nanoseconds()で現在のナノ秒タイムスタンプを取得し、それをチャネルcに送信します。
    • Tick(ns int64) *chan int64関数が定義されています。
      • これはtime.Tick()として外部に公開される関数です。
      • 新しいチャネルを作成し、そのチャネルと指定されたナノ秒間隔nsを引数としてTicker関数を新しいgoroutineで起動します。
      • 作成したチャネルを返します。このチャネルを通じて、呼び出し元は定期的な時間イベントを受け取ることができます。
  3. time/Makefileの変更:

    • src/lib/time/Makefileが更新され、新しく追加されたtick.gotick_test.goがビルドプロセスに含まれるようになりました。
    • 具体的には、O3という新しいオブジェクトリストが追加され、tick.$Oが含まれています。
    • time.atimeパッケージのアーカイブファイル)のビルドルールにa3が追加され、tick.$Oがリンクされるようになりました。
  4. time/tick_test.goの新規追加:

    • TestTickというテスト関数が定義されています。
    • Tick(Delta)でtickerを作成し、Count回チャネルから値を受け取ります。
    • 受け取った回数と間隔から期待される合計時間targetを計算し、実際の経過時間nsと比較します。
    • slop(許容誤差)を設けて、厳密な時間ではなくある程度のずれを許容しています。これは、OSのスケジューリングやシステムコールのオーバーヘッドにより、正確な時間間隔でのイベント発生が保証されないためです。

この初期実装では、select()システムコールをポーリングの代わりにタイムアウトとして使用している点が特徴的です。また、Ticker関数が無限ループで動作し、明示的な停止メカニズムがない点も、コメントで「tickerをキャンセルする方法」が必要であると述べられている理由です。

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

src/lib/syscall/time_amd64_darwin.go および src/lib/syscall/time_amd64_linux.go

--- a/src/lib/syscall/time_amd64_darwin.go
+++ b/src/lib/syscall/time_amd64_darwin.go
@@ -16,3 +16,9 @@ export func gettimeofday() (sec, nsec, errno int64) {
 	}\n \treturn r1, r2*1000, 0\n }\n+\n+export func nstotimeval(ns int64, tv *Timeval) {\n+\tns += 999;\t// round up\n+\ttv.sec = int64(ns/1000000000);\n+\ttv.usec = uint32(ns%1000000000 / 1000);\n+}\n```
(`time_amd64_linux.go`も同様の変更)

### `src/lib/time/Makefile`

```diff
--- a/src/lib/time/Makefile
+++ b/src/lib/time/Makefile
@@ -2,34 +2,70 @@
 # Use of this source code is governed by a BSD-style
 # license that can be found in the LICENSE file.\n \n+# DO NOT EDIT.  Automatically generated by gobuild.\n+# gobuild -m >Makefile\n O=6\n GC=$(O)g\n+CC=$(O)c -w\n+AS=$(O)a\n+AR=$(O)ar\n \n-PKG=$(GOROOT)/pkg/time.a\n+default: packages\n+\n+clean:\n+\trm -f *.$O *.a $O.out\n+\n+test: packages\n+\tgotest\n+\n+coverage: packages\n+\tgotest\n+\t6cov -g `pwd` | grep -v \'_test\\.go:\'\n+\n+%.$O: %.go\n+\t$(GC) $*.go\n+\n+%.$O: %.c\n+\t$(CC) $*.c\n+\n+%.$O: %.s\n+\t$(AS) $*.s\n \n O1=\\\n-\tzoneinfo.$O\n+\tzoneinfo.$O\\\n+\n O2=\\\
 \ttime.$O\\\
 \n-install: nuke $(PKG)\n+O3=\\\
+\ttick.$O\\\
+\n+time.a: a1 a2 a3\n \n-$(PKG): a1 a2\n+a1:\t$(O1)\n+\t$(AR) grc time.a zoneinfo.$O\n+\trm -f $(O1)\n \n-a1: \t$(O1)\n-\t$(O)ar grc $(PKG) $(O1)\n+a2:\t$(O2)\n+\t$(AR) grc time.a time.$O\n+\trm -f $(O2)\n \n-a2: \t$(O2)\n-\t$(O)ar grc $(PKG) $(O2)\n+a3:\t$(O3)\n+\t$(AR) grc time.a tick.$O\n+\trm -f $(O3)\n \n-$(O1): nuke\n+newpkg: clean\n+\t$(AR) grc time.a\n+\n+$(O1): newpkg\n $(O2): a1\n+$(O3): a2\n \n-nuke:\n-\trm -f *.$(O) *.a $(PKG)\n+nuke: clean\n+\trm -f $(GOROOT)/pkg/time.a\n \n-clean:\n-\trm -f *.$(O) *.a\n+packages: time.a\n+\n+install: packages\n+\tcp time.a $(GOROOT)/pkg/time.a\n \n-%.$O:\t%.go\n-\t$(GC) $<\ndiff --git a/src/lib/time/tick.go b/src/lib/time/tick.go\n```

### `src/lib/time/tick.go` (新規ファイル)

```go
// Copyright 2009 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 time

import (
	"syscall";
	"time"
)

// TODO(rsc): This implementation of time.Tick is a
// simple placeholder.  Eventually, there will need to be
// a single central time server no matter how many tickers
// are active.  There also needs to be a way to cancel a ticker.
//
// Also, if timeouts become part of the select statement,
// perhaps the Ticker is just:
//
//
//	func Ticker(ns int64, c *chan int64) {
//		for {
//			select { timeout ns: }
//			nsec, err := time.Nanoseconds();
//			c <- nsec;
//		}

func Ticker(ns int64, c *chan int64) {
	var tv syscall.Timeval;
	for {
		syscall.nstotimeval(ns, &tv);
		syscall.Syscall6(syscall.SYS_SELECT, 0, 0, 0, 0, syscall.TimevalPtr(&tv), 0);
		nsec, err := time.Nanoseconds();
		c <- nsec;
	}
}

export func Tick(ns int64) *chan int64 {
	c := new(chan int64);
	go Ticker(ns, c);
	return c;
}

src/lib/time/tick_test.go (新規ファイル)

// Copyright 2009 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 time

import (
	"testing";
	"time";
)

export func TestTick(t *testing.T) {
	const (
		Delta uint64 = 10*1e6;
		Count uint64 = 10;
	);
	c := Tick(Delta);
	t0, err := Nanoseconds();
	for i := 0; i < Count; i++ {
		<-c;
	}
	t1, err1 := Nanoseconds();
	ns := t1 - t0;
	target := int64(Delta*Count);
	slop := target*2/10;
	if ns < target - slop || ns > target + slop {
		t.Fatalf("%d ticks of %d ns took %d ns, expected %d", Count, Delta, ns, target);
	}
}

コアとなるコードの解説

このコミットの核心は、src/lib/time/tick.goで定義されているTicker関数とTick関数です。

  1. Ticker関数:

    • func Ticker(ns int64, c *chan int64): この関数は、指定されたナノ秒間隔nsごとに、現在の時刻をcチャネルに送信する役割を担います。
    • var tv syscall.Timeval;: selectシステムコールに渡すタイムアウト値を格納するためのTimeval構造体を宣言しています。
    • for { ... }: 無限ループで、定期的なイベント生成を継続します。
    • syscall.nstotimeval(ns, &tv);: ns(ナノ秒)をTimeval構造体に変換します。この変換は、OSのselectシステムコールが秒とマイクロ秒でタイムアウト値を指定するため必要です。
    • syscall.Syscall6(syscall.SYS_SELECT, 0, 0, 0, 0, syscall.TimevalPtr(&tv), 0);: ここが最も重要な部分です。selectシステムコールを呼び出し、指定されたtvの時間だけ待機します。最初の4つの0は、監視するファイルディスクリプタがないことを意味し、このselectは純粋にタイムアウトとして機能します。
    • nsec, err := time.Nanoseconds();: selectがタイムアウトして待機が解除された後、現在のナノ秒単位の時刻を取得します。
    • c <- nsec;: 取得した時刻をチャネルcに送信します。これにより、Tick関数を呼び出した側は、このチャネルから定期的に時刻イベントを受け取ることができます。
  2. Tick関数:

    • export func Tick(ns int64) *chan int64: この関数は、外部(timeパッケージの利用者)に公開されるインターフェースです。
    • c := new(chan int64);: 新しいint64型のチャネルを作成します。このチャネルが、定期的な時刻イベントを配信するためのパイプとなります。
    • go Ticker(ns, c);: Ticker関数を新しいgoroutineとして起動します。これにより、Ticker関数はバックグラウンドで非同期に動作し、メインのプログラムフローをブロックすることなく、定期的にチャネルに時刻を送信し続けます。
    • return c;: 作成したチャネルを返します。利用者はこのチャネルを読み取ることで、ns間隔で発生するイベントを処理できます。

この実装は、Goの並行処理モデル(goroutineとchannel)を効果的に利用して、シンプルながらも機能的な定期イベント通知メカニズムを実現しています。ただし、コメントにあるように、この初期バージョンには「tickerをキャンセルする方法」がないなど、いくつかの改善点が残されていました。後のGoのバージョンでは、time.NewTickertime.Afterといったより柔軟で堅牢なAPIが提供されるようになります。

関連リンク

  • Go言語の公式ドキュメント(timeパッケージ): https://pkg.go.dev/time
  • Go言語の並行処理に関する公式ブログ記事(初期のもの): https://go.dev/blog/concurrency-is-not-parallelism (概念的な理解に役立ちます)
  • select()システムコールに関するmanページ(Linuxの場合): man 2 select (ターミナルで実行)

参考にした情報源リンク