【C#】System.Threading.Timerの精度を確認する

タイトルの通り、C# の System.Threading.Timer の実行間隔に 1msを指定したとき実際には 1ms では実行されず、15~16ms くらいの間隔で実行されるようになる現象の確認になります。

他に Thread.Sleep や System.Timers.Timer なんかの時間を指定する処理にも同じ状況となります。

確認環境

  • .NET Frameekwoek 3.5
  • Visual Studio 2012
  • Windows 7(2017/03現在)

確認コード

まず、確認前の前提として Windows 上で動作する C# のこういった時間の最小解像度は Windows のシステムクロック解像度(system clock resolution)nの影響を受けています。既定値が 15ms(だったと思います).NET Framework のこういった処理はこの解像度の影響を受けて下限となっているようです。

上記仮定を検証するためにの 1ミリ秒のタイマー実行要求を 100回繰り返すコードを書いてみました。

// 時間計測用のタイマー
private static Stopwatch sw = new Stopwatch();
// 結果の書き出し先
private static StreamWriter log;
// 測定対象のタイマー
private static Timer timer;
// 総実行回数
private static int total;
// 実行回数
private static int cnt;
// スレッド待機用のハンドル
private static AutoResetEvent thHandle = new AutoResetEvent(false);

internal static void Main(string[] args)
{
    using (log = new StreamWriter(@"d:\log.csv"))
    {
        // 繰り返し計測用のループ
        //for (int j = 0; j < 1; j++)
        //{
        //    for (int i = 1; i <= 1; i++)
        //    {
                timer = new Timer(threading_timer_callback, 1, 0, 1); // 1msを指定

                thHandle.WaitOne();
        //    }
        //}
    }

    Console.WriteLine("Finish");
    Console.ReadLine();
}

private static void threading_timer_callback(object state)
{
    int i = (int)state;

    if (cnt > 100)
    {
        cnt = 0;
        timer.Change(Timeout.Infinite, 0);
        timer.Dispose();
        thHandle.Set();
    }

    sw.Stop();

    // 前回終了時からの経過時間
    log.WriteLine(total + ", " + cnt + ", " + i + ", " + sw.Elapsed.TotalMilliseconds);
    log.Flush();

    total++;
    cnt++;

    sw.Reset();
    sw.Start();
}

1ms を指定した実行結果ですが、概ね16msで実行されて、たまに14msで実行されます。

では次に、15ms で割り切れない数の 32msを指定した場合を見てみたいと思います。

このように発生タイミングがめちゃくちゃになります。32ms で稀に実行され 44~ 48ms で発生することが多くばらつきが非常に大きくなります。

スケジュールの関係上、何回に1回かはジャストで発生するようですが大体、次のタイマー周期に回されて 32 + 15ms 近辺で発生します。

31msを指定した場合こんな感じになります。

今度はかなり指定値に近い結果が出ました。31 or 32に集中しています。

というわけでこの記事を書いている PC上で .NET Frameowowk のタイマーの精度は 15ms 程度だったという事が分かったので、確認結果をまとめると

  • 1ms 周期のタイマーは発生しない
    • 15ms 以下を指定しても 15~16ms程度に無視される
  • 発生周期はシステムクロック解像度の倍数(+各種遅延)で発生する(可能性が高い)
  • 次回割り込みに回されると+割り込み周期の誤差が発生する
    • 今回の場合15ミリ秒(程度)、後で実行される可能性がある

.NETのコード読んでないので推測だらけになってしまいましたが(いや、軽く読んだのですが)、少なくとも検証した環境でTimerを使った場合、正しく精度が高いタイマー動作を期待してプログラムを書いてはいけないという事が分かりました。

関連記事・参考資料

takachan.hatenablog.com

旧MSDN リンクが切れていたので更新しました。 この記事にはあまり関係ないですがシステムクロック解像度について色々面白いことが書いてあるので読んで損はないと思います。

learn.microsoft.com

最近似たような別の記事を書きました。

takap-tech.com