yikegaya’s blog

仕事関連(Webエンジニア)と資産運用について書いてます

「Goならわかるシステムプログラミング」の16章を読んだ(時間と時刻)

OSが使う時間の仕組みは歴史的経緯などもあって少し複雑な構成をしており、たくさんの種類のタイマーやカウンターがあります。

  • リアルタイムクロック(RTC)
  • システムクロック
  • タイムスタンプカウンター(TSC
  • 各種タイマーデバイス

時間に関するシステムコール

OSの中の仕組みは複雑ですが、Go言語のランタイムと時間関係の接点はシンプルです。Go言語には時刻関連のランタイム関数がいくつかありますが、低レベルな機能として主に使われているのはruntime.now()と、runtime.semasleep()の2つです。

runtime.now()

Windows の 場 合 、7ffe0000 番 地 か ら の 1 キ ロ バ イ ト ほ ど の 領 域 は 、SharedUserDataという読み込み専用のデータ領域として、プロセスにマッピングされています。実体はカーネル内部にあります。第15章で紹介した、仮想メモリを使ったメモリ共有です。この領域の先頭から20バイト(0x14)めからの領域が、SystemTimeというシステム時間が保存される領域となっています。Windowsカーネルが100ナノ秒の精度でこのメモリ領域のカウンターを更新するので、Goのプログラムではこのアドレスを参照することで現在時刻を取得します。 Linux の場合は、clock_gettime() と gettimeofday() という 2 つのシステムコールが使われます。Go 言語では、clock_gettime() が利用できたらまず利用し、利用できなければgettimeofday()にフォールバックします。Linuxカーネルには、頻繁に使われるシステムコールのために、vDSO(仮想ELF動的共有オブジェクト)という高速な呼び出しの仕組みが用意されています。clock_gettime()とgettimeofday()も、このvDSOを利用してシステムコールのオーバーヘッドがなく呼び出せるようになっています。Go 言語で利用しているのも、この vDSO 版のclock_gettime()とgettimeofday()です。

runtime.semasleep()

Go のタイマー処理を使用したときに最終的に呼び出されるのは、runtime.semasleep() という関数です。この関数では、マルチスレッドの共有資源の管理 に使われるセマフォと呼ばれる仕組みを利用しています。通常、セマフォを使うときは、正常ケースで資源管理を獲得し、期待どおりにいかなかったケースでタイムアウトさせますが、Goのタイマーでは逆にタイムアウトの機構を使って処理待ちを行っています。

Go言語で時間を扱う

// 5 秒
5 * time.Second
// 10 ミリ秒
10 * time.Millisecond
// 10 分 30 秒
time.ParseDuration("10m30s")
// 現在時刻
time.Now()
// 指定日時を作成
time.Date(2017, time.August, 26, 11, 50, 30, 0, time.Local)
// フォーマットを指定してパース(後述)
time.Parse(time.Kitchen, "11:30AM")
// Epoch タイム(後述)から作成
time.Unix(1503673200, 0)
// 3 時間後の時間
fmt.Println(time.Now().Add(3 * time.Hour))
// ファイル変更日時が何日前か知る
fileInfo, _ := os.Stat(".vimrc")
fmt.Printf("%v 前 ", time.Now().Sub(fileInfo.ModTime()))
// 時間を 1 時間ごとに丸める
fmt.Println(time.Now().Round(time.Hour))

スリープ

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("waiting 5 seconds")
    time.Sleep(5 * time.Second)
    fmt.Println("done")
}

チャネルを使ったタイマー

package main

import (
  "fmt"
  "time"
)

func main() {
  fmt.Println("waiting 5 seconds")
  after := time.After(5 * time.Second)
  <-after
  fmt.Println("done")
}

time.Tickで通知

package main

import (
  "fmt"
  "time"
)
func main() {
    fmt.Println("waiting 5 seconds")
  for now := range time.Tick(5 * time.Second) {
    fmt.Println("now:", now)
  }
}

時刻のフォーマット

時刻をテキスト化したいとき、Go言語以外では、何かしらのプレースホルダを使って表現するのが一般的です。たとえば、%YやYYYYといったプレースホルダを他のプログラミング言語で使ったことがある人は多いでしょう。これに対し、Go言語では、数値を使ったテキストで時刻のフォーマットを指定します。

package main

import (
    "fmt"
    "time"
)

func main() {
    now := time.Now()
    fmt.Println(now.Format(time.RFC822))
    // 27 Aug 17 11:31 JST

    fmt.Println(now.Format("2006/01/02 03:04:05 MST"))
    // 2017/08/27 11:31:53 JST
}