[インデックス 1298] ファイルの概要
このコミットは、Go言語のtimeパッケージにおける重要な改善を含んでいます。主な目的は、time.Tick関数における時間のずれ(スキュー)を回避するためのロジックを導入すること、そしてtime.Secondsおよびtime.Nanoseconds関数からエラー返却を削除し、よりシンプルなAPIを提供することです。これにより、タイマーの精度が向上し、時間取得関数の使い勝手が改善されます。
コミット
commit 6478df1c418421cd3a148f77d732ce4c57486314
Author: Russ Cox <rsc@golang.org>
Date: Mon Dec 8 17:45:50 2008 -0800
avoid skew in time.Tick; remove errors from time.Seconds, time.Nanoseconds
R=r
DELTA=46 (21 added, 10 deleted, 15 changed)
OCL=20785
CL=20787
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6478df1c418421cd3a148f77d732ce4c57486314
元コミット内容
avoid skew in time.Tick; remove errors from time.Seconds, time.Nanoseconds
変更の背景
このコミットは、Go言語の初期開発段階におけるtimeパッケージの成熟化の一環として行われました。
-
time.Tickにおけるスキューの問題:time.Tickは、指定された間隔でイベントを発生させるためのタイマー機能を提供します。しかし、以前の実装では、タイマーのコールバック処理やシステムコールにかかるわずかな時間によって、次のイベント発生時刻が徐々に遅れていく「スキュー(ずれ)」が発生する可能性がありました。例えば、100ミリ秒ごとにイベントを発生させたい場合でも、各イベント処理に1ミリ秒かかると、実際には101ミリ秒ごとにイベントが発生し、時間が経つにつれて累積的な遅延が生じます。このコミットは、この累積的なずれを積極的に補正するロジックを導入し、より正確な周期タイマーを実現することを目的としています。 -
time.Secondsおよびtime.Nanosecondsのエラー返却の削除: Go言語の設計哲学の一つに「エラーは明示的に処理する」というものがありますが、同時に「パニックは回復不能なエラーにのみ使用する」という原則もあります。初期のtimeパッケージでは、SecondsやNanosecondsといった時間取得関数がos.Errorを返していました。しかし、これらの関数がOSから時間を取得する際にエラーが発生するケースは非常に稀であり、かつ、そのエラーから回復することは通常不可能です。このような状況でエラーを返すと、呼び出し側で常にエラーチェックを行う必要があり、コードが冗長になります。この変更は、このような回復不能なエラーの場合にはパニック(panic)を発生させることで、APIを簡潔にし、一般的な使用シナリオでのエラーハンドリングの負担を軽減することを意図しています。これは、Go言語におけるエラーハンドリングのイディオムが確立されていく過程で見られた典型的な改善の一つです。
前提知識の解説
time.Tick: Go言語のtimeパッケージで提供される関数で、指定された期間ごとに現在時刻をチャネルに送信するタイマーを作成します。例えば、time.Tick(time.Second)は1秒ごとに現在時刻を送信するチャネルを返します。- スキュー (Skew): 時間ベースの処理において、期待される周期からのずれや遅延が累積していく現象を指します。特に、定期的なタスクを実行する際に、タスク自体の実行時間やシステムコールなどのオーバーヘッドによって、次のタスクの開始時刻が徐々に遅れていく場合に問題となります。
syscall.SYS_SELECT: Unix系システムコールの一つで、複数のファイルディスクリプタ(この場合はタイマーイベント)の準備ができるまで待機するために使用されます。ここでは、指定された時間(tvで設定)が経過するまで処理をブロックするために利用されています。syscall.Timeval:selectシステムコールなどで使用される時間構造体で、秒とマイクロ秒で時間を表現します。time.Seconds()/time.Nanoseconds(): Go言語のtimeパッケージで提供される関数で、それぞれUnixエポック(1970年1月1日00:00:00 UTC)からの経過秒数またはナノ秒数を返します。os.Error: Go言語の初期バージョンで使用されていたエラー型です。現在のerrorインターフェースの前身にあたります。panic: Go言語における回復不能なエラー処理メカニズムです。プログラムの実行を即座に停止させ、スタックトレースを出力します。通常、予期せぬプログラミングエラーや、続行が不可能な状況で使用されます。
技術的詳細
このコミットの技術的詳細は、主にtime.Tickのスキュー回避ロジックと、時間取得関数のエラーハンドリングの変更に集約されます。
time.Tickにおけるスキュー回避
以前のTicker関数は、単に指定された間隔(ns)でsyscall.SYS_SELECTを呼び出し、その後に現在時刻を取得してチャネルに送信していました。この方法では、syscall.SYS_SELECTの待機時間と、その後の処理(時間取得、チャネル送信など)にかかる時間が合算され、次の待機開始時刻がずれていきました。
新しい実装では、以下のロジックが導入されています。
when変数の導入:whenは、次にイベントを発生させるべき「目標時刻」(絶対時刻)をナノ秒単位で保持します。- 目標時刻の計算:
when += nsによって、次の目標時刻を計算します。 - スキューの補正:
if when < now: もし現在の時刻(now)が既に目標時刻(when)を過ぎてしまっている場合(つまり、前回の処理が長すぎた場合)、目標時刻を大きく進めます。when += (now-when)/ns * nsは、現在の時刻から目標時刻までの遅延をnsの倍数で補正し、目標時刻を現在の時刻に最も近い未来の周期に合わせます。for when <= now: 上記の大きな補正だけでは不十分な場合(例えば、nsが非常に小さい場合や、処理が極端に遅れた場合)、目標時刻が現在の時刻を過ぎている間、nsずつ目標時刻を進めます。これにより、目標時刻が必ず現在の時刻より未来になるように調整されます。
- 待機時間の計算:
syscall.nstotimeval(when - now, &tv)によって、次にsyscall.SYS_SELECTで待機すべき時間を計算します。これは、現在の時刻から目標時刻までの残り時間です。これにより、タイマーは常に「目標時刻」に到達するように調整され、累積的なスキューが防止されます。 Tick関数の引数チェック:Tick関数に渡される間隔nsが0以下の場合にnilを返すようになりました。これは、無限ループや不適切なタイマー動作を防ぐための堅牢性向上です。
time.Secondsおよびtime.Nanosecondsのエラー返却の削除
以前のSecondsおよびNanoseconds関数は、内部で呼び出すos.Time()が返すos.Errorをそのまま返していました。
変更後:
- これらの関数は
int64のみを返すようになりました。 os.Time()がエラーを返した場合、panicを発生させるようになりました。具体的には、panic("time: os.Time: ", err.String())という形式でパニックメッセージを出力します。
これは、Go言語の設計思想において、回復不能なエラーはパニックとして扱うという方針に沿ったものです。OSからの時間取得に失敗するような状況は、通常、システムレベルの深刻な問題であり、プログラムが続行できる状態ではありません。このような場合にエラーを返して呼び出し側に処理を委ねるよりも、即座にパニックさせて問題を明確にする方が適切であると判断されました。
time.UTC()およびtime.LocalTime()の簡素化
Seconds()関数がエラーを返さなくなったため、UTC()およびLocalTime()関数もその恩恵を受け、エラーハンドリングのロジックが不要になり、コードが大幅に簡素化されました。
コアとなるコードの変更箇所
src/lib/time/tick.go
--- a/src/lib/time/tick.go
+++ b/src/lib/time/tick.go
@@ -26,15 +26,32 @@ import (
func Ticker(ns int64, c *chan int64) {
var tv syscall.Timeval;
+ now := time.Nanoseconds();
+ when := now;
for {
- syscall.nstotimeval(ns, &tv);
+ when += ns; // next alarm
+
+ // if c <- now took too long, skip ahead
+ if when < now {
+ // one big step
+ when += (now-when)/ns * ns;
+ }
+ for when <= now {
+ // little steps until when > now
+ when += ns
+ }
+
+ syscall.nstotimeval(when - now, &tv);
syscall.Syscall6(syscall.SYS_SELECT, 0, 0, 0, 0, syscall.TimevalPtr(&tv), 0);
- nsec, err := time.Nanoseconds();
- c <- nsec;
+ now = time.Nanoseconds();
+ c <- now;
}
}
export func Tick(ns int64) *chan int64 {
+ if ns <= 0 {
+ return nil
+ }
c := new(chan int64);
go Ticker(ns, c);
return c;
src/lib/time/tick_test.go
--- a/src/lib/time/tick_test.go
+++ b/src/lib/time/tick_test.go
@@ -15,11 +15,11 @@ export func TestTick(t *testing.T) {
Count uint64 = 10;
);
c := Tick(Delta);
- t0, err := Nanoseconds();
+ t0 := Nanoseconds();
for i := 0; i < Count; i++ {
<-c;
}
- t1, err1 := Nanoseconds();
+ t1 := Nanoseconds();
ns := t1 - t0;
target := int64(Delta*Count);
slop := target*2/10;
src/lib/time/time.go
--- a/src/lib/time/time.go
+++ b/src/lib/time/time.go
@@ -10,17 +10,21 @@ import (
// Seconds since January 1, 1970 00:00:00 GMT
-export func Seconds() (sec int64, err *os.Error) {
- var nsec int64;
- sec, nsec, err = os.Time();
- return sec, err
+export func Seconds() int64 {
+ sec, nsec, err := os.Time();
+ if err != nil {
+ panic("time: os.Time: ", err.String());
+ }
+ return sec
}
// Nanoseconds since January 1, 1970 00:00:00 GMT
-export func Nanoseconds() (nsec int64, err *os.Error) {
- var sec int64;
- sec, nsec, err = os.Time();
- return sec*1e9 + nsec, err
+export func Nanoseconds() int64 {
+ sec, nsec, err := os.Time();
+ if err != nil {
+ panic("time: os.Time: ", err.String());
+ }
+ return sec*1e9 + nsec
}
export const (
@@ -142,12 +146,7 @@ export func SecondsToUTC(sec int64) *Time {
}
export func UTC() (t *Time, err *os.Error) {
- var sec int64;
- sec, err = Seconds();
- if err != nil {
- return nil, err
- }
- return SecondsToUTC(sec), nil
+ return SecondsToUTC(Seconds()), nil
}
// TODO: Should this return an error?
@@ -163,12 +162,7 @@ export func SecondsToLocalTime(sec int64) *Time {
}
export func LocalTime() (t *Time, err *os.Error) {
- var sec int64;
- sec, err = Seconds();
- if err != nil {
- return nil, err
- }
- return SecondsToLocalTime(sec), nil
+ return SecondsToLocalTime(Seconds()), nil
}
// Compute number of seconds since January 1, 1970.
コアとなるコードの解説
src/lib/time/tick.go
Ticker関数内のスキュー回避ロジック:now := time.Nanoseconds();とwhen := now;: 現在のナノ秒時刻をnowに、次回の目標時刻の基準となるwhenを初期化します。when += ns;: ループの開始時に、次回のイベント発生目標時刻を計算します。if when < now { when += (now-when)/ns * ns; }: もし目標時刻whenが現在の時刻nowよりも過去になってしまっている場合(つまり、前回の処理が長引いたためスキューが発生した場合)、whenをnsの倍数で現在の時刻に最も近い未来の周期に「ジャンプ」させます。これにより、大きなスキューを一度に補正します。for when <= now { when += ns }: 上記の補正後もまだwhenがnow以下の場合(例えば、nsが非常に小さいか、処理が極端に遅れた場合)、whenがnowより大きくなるまでnsずつ加算します。これにより、目標時刻が常に未来になるように微調整します。syscall.nstotimeval(when - now, &tv);:syscall.SYS_SELECTで待機すべき時間を、現在の時刻から目標時刻までの残り時間として正確に計算します。now = time.Nanoseconds(); c <- now;:syscall.SYS_SELECTから戻った後、再度現在の時刻を取得し、それをチャネルに送信します。このnowは次のループのwhen計算の基準となります。
Tick関数内の引数チェック:if ns <= 0 { return nil }:Tick関数に0以下の間隔が渡された場合、タイマーを生成せずにnilを返します。これは、無限ループや不適切なタイマー動作を防ぐためのガードです。
src/lib/time/tick_test.go
t0, err := Nanoseconds();とt1, err1 := Nanoseconds();がそれぞれt0 := Nanoseconds();とt1 := Nanoseconds();に変更されています。これは、Nanoseconds()関数がエラーを返さなくなったことに伴うテストコードの修正です。
src/lib/time/time.go
Seconds()関数:- 返り値の型が
(sec int64, err *os.Error)からint64に変更されました。 os.Time()の呼び出し結果にエラーが含まれる場合、panic("time: os.Time: ", err.String());を呼び出してプログラムを終了させるようになりました。
- 返り値の型が
Nanoseconds()関数:- 返り値の型が
(nsec int64, err *os.Error)からint64に変更されました。 os.Time()の呼び出し結果にエラーが含まれる場合、panic("time: os.Time: ", err.String());を呼び出してプログラムを終了させるようになりました。
- 返り値の型が
UTC()関数とLocalTime()関数:- 内部で
Seconds()関数を呼び出す際に、エラーチェックのロジックが完全に削除されました。これは、Seconds()がエラーを返さなくなり、代わりにパニックを発生させるようになったため、呼び出し側でエラーを処理する必要がなくなったためです。これにより、コードが大幅に簡素化されました。
- 内部で
関連リンク
- Go言語の
timeパッケージに関する公式ドキュメント(現在のバージョン): https://pkg.go.dev/time - Go言語のエラーハンドリングに関する一般的な議論: https://go.dev/blog/error-handling-and-go
参考にした情報源リンク
- Go言語のソースコード(GitHubリポジトリ): https://github.com/golang/go
- Go言語の初期のコミット履歴
- Go言語における
panicとerrorの使い分けに関する一般的な知識 - タイマーのスキューに関する一般的なプログラミング概念