ProgrammingGo開発者

time.Sleepとtime.Afterを使ったGoにおける作業の特性を説明してください:違いは何ですか?何を適用するべきですか?競合プログラムでtime.Sleepを使用する際の落とし穴は何ですか?

Hintsage AIアシスタントで面接を突破

答え。

質問の背景:

初期からGo言語はtimeパッケージを提供しており、その中に時間を管理するための基本的な関数、time.Sleeptime.Afterがあります。System.sleepのある言語とは異なり、Goはそのプリミティブを使って非同期タイマーを実装しており、これがマルチスレッドでの作業において重要です。

問題点:

多くの開発者はタスク間のポーズを実現するためにtime.Sleepを不正に使用しがちですが、これは競合プログラムでは望ましくありません。Goのパラダイムでは、select/チャネルとの統合のためにチャンネルとtime.Afterを使ってイベント待機の管理を行うのが正しいです。

解決策:

time.Sleep(d)は現在のゴルーチンをd秒間ブロックしますが、これは直接的な「睡眠」です。time.After(d)は、d秒後にイベントが発生するチャネルを返します。後者の選択肢は、チャネルによるselectにおいてはるかに柔軟で、中断可能な待機やタイムアウトに便利です。

コード例:

ch := make(chan struct{}) go func() { time.Sleep(2 * time.Second) ch <- struct{}{} }() select { case <-ch: fmt.Println("done") case <-time.After(1 * time.Second): fmt.Println("timeout") }

主な特性:

  • time.Afterはselectと組み合わせることでタイムアウトを実現するのに優れています。
  • time.Sleepは現在のゴルーチンだけをブロックし、スレッドやプロセスはブロックしません。
  • time.Afterは毎回新しいチャネルを作成し、古いチャネルは値が消費されるまで解放されません。

ひねりのある質問。

time.Sleepを使用してプログラム全体をブロックできますか?

いいえ、time.Sleepは「眠り」に入るのは1つのゴルーチンだけで、他は引き続き動作します。

time.Afterがチャネルが読み取られない場合にメモリリークを引き起こすことがありますか?

はい、値が読み取られるまでタイマーは残ります — 読み取りがない場合、ガーベジコレクタはオブジェクトを削除しません。

コード例:

func leak() { for { _ = time.After(time.Hour) } }

time.Afterでイベントを受信しない場合の待機を正しくキャンセルする方法は?

time.Timerを使用し、必要に応じて手動で停止するのがベストです:

t := time.NewTimer(time.Minute) if done { t.Stop() }

一般的な間違いやアンチパターン

  • チャネルやselectの代わりにワーカーゴルーチンでtime.Sleepを使用する。
  • time.Afterの値を読み取らず、タイマーをリークさせる。
  • time.Afterをループ内で作成し、値を消費する余裕がない。

実生活の例

ネガティブケース

ゴルーチンは作業からの信号を待ち、タイムアウトのために次のようにします:

time.Sleep(10*time.Second) doSomething()

利点:

  • 単純に時間のポーズを追加できる。

欠点:

  • 信号がすでに受信されている場合や作業が不要な場合 — 不要な睡眠、タイミングバグの可能性。
  • すべてはキャンセル不可能で、タイムアウトが発生する前にキャンセルが実行された場合に「目覚め」させるのが難しい。

ポジティブケース

コードはselectとtime.Afterを通じて構築されます:

select { case <-workSignal: // 実行する case <-time.After(10 * time.Second): // タイムアウトによる実行 }

利点:

  • 待機とタイムアウトは互換性があり、キャンセルをシミュレートしやすい。
  • 停止時間を最小限に抑えられる。

欠点:

  • コードを読むのが少し難しく、selectとチャネルの動作を理解する必要がある。