PG日誌

読者です 読者をやめる 読者になる 読者になる

PG日誌

主にc#の事を書いています

C#のSystem.Threading.Timerクラスの精度を確認する


この話は、C#のSystem.Threading.Timerクラスの定周期処理に限ったことではないのですが、タイマーのインターバールに1msを指定したときの実際の実行間隔の話です。

まず、確認前の前提としてタイマー動作はPC物理的な「ハードウェアタイマー」とそれを制御しているWindowsの「HAL(Hardware Abstraction Layer)」ドライバに関係していると考えています。

で、HALは周期が16ms(ハードによっては10msとかもある)となっていて、その仕組み上に乗っている(と思われる)C#のタイマーの精度は前述の数値に依存するかなと。

従って、タイトルの1msを指定した場合、割り込み周期に同期して1msのタイマーは発生せず、16msでタイマーが発生すると仮定し、これが本当に正しいかを確認します。

なので、上記過程を検証するための、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();
}

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

f:id:Takachan:20170330002220p:plain

で、もう少し発展させて、じゃあ16ミリ秒以上を指定したらどうなるんだという問題です。

32msを指定しました。

f:id:Takachan:20170330003243p:plain

まぁ、こんな感じに、めちゃくちゃになります。

何回に1回かはジャストで発生しますが、大体、次の割り込み周期に回されて32+16ms近辺で発生します。

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

f:id:Takachan:20170330003929p:plain

今度はかなり希望に近い結果が出ました。

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

  • 1ms周期のタイマーは発生しない
    • 16ms以下、10ms以下のタイマーは(HAL的に?)無理(と思われる)
  • 発生周期は割り込み周期の倍数で発生する(可能性が高い)
  • 次回割り込みに回されると+割り込み周期の誤差が発生する
    • 今回の場合16ミリ秒(程度)、後で実行される可能性がある
  • .NET COREでほかのOSの場合、結果が全然異なると思われる

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