[インデックス 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の低レベルな時間管理機能を組み合わせています。
-
nstotimeval
関数の追加:src/lib/syscall/time_amd64_darwin.go
とsrc/lib/syscall/time_amd64_linux.go
にnstotimeval
関数が追加されました。- この関数は、ナノ秒単位の時間(
ns
)を受け取り、それをTimeval
構造体(秒とマイクロ秒)に変換します。 ns += 999;
という行は、ナノ秒をマイクロ秒に変換する際に、切り上げを行うためのものです。例えば、1000ナノ秒未満の値をマイクロ秒に変換する際に、0にならないように調整しています。tv.sec = int64(ns/1000000000);
で秒を計算し、tv.usec = uint32(ns%1000000000 / 1000);
でマイクロ秒を計算しています。
-
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_SELECT
はselect()
システムコールに対応する定数です。- 引数の
0, 0, 0, 0
は、監視するファイルディスクリプタセット(読み込み、書き込み、例外)が空であることを示します。つまり、このselect()
呼び出しはI/Oイベントを待つのではなく、純粋にタイムアウト機能として利用されています。 syscall.TimevalPtr(&tv)
は、タイムアウト時間を指定するためのポインタです。
select()
がタイムアウトすると、time.Nanoseconds()
で現在のナノ秒タイムスタンプを取得し、それをチャネルc
に送信します。
- この関数は無限ループで動作し、指定された
Tick(ns int64) *chan int64
関数が定義されています。- これは
time.Tick()
として外部に公開される関数です。 - 新しいチャネルを作成し、そのチャネルと指定されたナノ秒間隔
ns
を引数としてTicker
関数を新しいgoroutineで起動します。 - 作成したチャネルを返します。このチャネルを通じて、呼び出し元は定期的な時間イベントを受け取ることができます。
- これは
- このファイルは
-
time/Makefile
の変更:src/lib/time/Makefile
が更新され、新しく追加されたtick.go
とtick_test.go
がビルドプロセスに含まれるようになりました。- 具体的には、
O3
という新しいオブジェクトリストが追加され、tick.$O
が含まれています。 time.a
(time
パッケージのアーカイブファイル)のビルドルールにa3
が追加され、tick.$O
がリンクされるようになりました。
-
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
関数です。
-
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
関数を呼び出した側は、このチャネルから定期的に時刻イベントを受け取ることができます。
-
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.NewTicker
やtime.After
といったより柔軟で堅牢なAPIが提供されるようになります。
関連リンク
- Go言語の公式ドキュメント(timeパッケージ): https://pkg.go.dev/time
- Go言語の並行処理に関する公式ブログ記事(初期のもの): https://go.dev/blog/concurrency-is-not-parallelism (概念的な理解に役立ちます)
select()
システムコールに関するmanページ(Linuxの場合):man 2 select
(ターミナルで実行)
参考にした情報源リンク
- Go言語のソースコード(特に
time
パッケージの進化) - Unix/Linuxの
select()
システムコールに関するドキュメント - Go言語の初期の設計に関する議論やメーリングリストのアーカイブ(公開されている場合)
- Go言語の並行処理に関する一般的な解説記事や書籍
- https://pkg.go.dev/time (Go timeパッケージの現在のドキュメント)
- https://go.dev/blog/ (Go公式ブログ)
- https://man7.org/linux/man-pages/man2/select.2.html (Linux
select
man page) - https://en.wikipedia.org/wiki/Select_(Unix) (Wikipedia: Select (Unix))
- https://go.dev/doc/effective_go#concurrency (Effective Go: Concurrency)