【C#】Timerの精度にtimeBeginPeriodが効くのか確認する

System.Threading.Timer クラスで定周期に実行されるコールバックの実行間隔に対して、システムクロック解像度を指定する timeBeginPeriod が影響を与えるかどうかが気になったので検証してみたいと思います。*1 *2

通常だと、Timer の実行間隔を 1msを指定しても実行間隔は約 15.6ms 前後で実行されますが、timeBeginPeriod で解像度を1に変更した際にハンドラーの実行間隔が変わるかどうかを確認したいと思います(と言うのも参考資料のページを読むと聞くのか聞かないのかいまいち判然としない書き方なので自分で実装して確かめようと思います)

確認環境

この記事は以下の環境で確認しています。

  • .NET 10
  • Visual Studio 2026
  • Windows11

検証用コード

最初に 1ms周期に設定して、100回実行、次に解像度を変更してさらに 100回実行のような実装になっています。

using System.Diagnostics;
using System.Runtime.InteropServices;

internal partial class Program
{
    // システムクロック解像度を変更するWin32APIの定義
    [LibraryImport("winmm.dll", EntryPoint = "timeBeginPeriod")]
    private static partial uint TimeBeginPeriod(uint uPeriod);

    // システムクロック解像度を復元するWin32APIの定義
    [LibraryImport("winmm.dll", EntryPoint = "timeEndPeriod")]
    private static partial uint TimeEndPeriod(uint uPeriod);

    static async Task Main(string[] args)
    {
        // (1) デフォルトのシステムクロック解像度で 1msを指定して実行
        await Start(100, TimeSpan.FromMilliseconds(1));

        Console.WriteLine("----");

        // (2) 解像度を1msに変更して 1ms周期で実行
        _ = TimeBeginPeriod(1);
        await Start(100, TimeSpan.FromMilliseconds(1));

        _ = TimeEndPeriod(1);
    }

    // 指定回数分タイマーを実行して結果をコンソールに表示する
    static async Task Start(int count, TimeSpan interval)
    {
        // 終わりを待つ用
        TaskCompletionSource tcs =
            new(TaskCreationOptions.RunContinuationsAsynchronously);

        // 現在の実行回数
        int currentCount = 0;

        // 実行時間を入れる
        List<double> result = new(count);

        // 実行時間計測用
        Stopwatch sw = Stopwatch.StartNew();

        // ウォームアップで読み捨てる回数
        int warmupCnt = 5;

        // タイマーを作成して指定のインターバルで動作開始
        Timer? timer = null;
        try
        {
            timer = new Timer(_ =>
            {
                try
                {
                    if (currentCount++ < warmupCnt)
                    {
                        return; // 初回起動の不安定状態は読み捨て
                    }

                    // 前回からの経過時間を記録
                    double ms = sw.Elapsed.TotalMilliseconds;
                    result.Add(ms);

                    // 規定回数になったら終了
                    if (currentCount >= count + warmupCnt)
                    {
                        timer?.Change(-1, -1);
                        tcs.TrySetResult();
                    }
                }
                finally
                {
                    sw.Restart();
                }
            },
            null,
            interval, interval);

            await tcs.Task; // 終わるまで待ち

            // 最後に一括で出力
            for (int i = 0; i < result.Count; i++)
            {
                Console.WriteLine($"{i:D3}, {result[i]}");
            }
        }
        finally
        {
            timer?.Dispose();
        }
    }
}

検証結果

1) 未指定時

これは想定通り 15.6msに近い形になりました。

  • 平均: 15.79ms
  • 最小: 14.87ms
  • 最大: 16.09ms

2) timeBeginPeriod=1指定時

どうやら解像度の指定がタイマーに対して効いているようです。

  • 平均: 1.45ms
  • 最小: 0.98ms
  • 最大: 2.02ms

まとめ

公式ドキュメントを読むとどっちだよ、、、みたいな雰囲気でしたがこの環境では timeBeginPeriod は System.Threading,Timer に対して効果がある。という結論でした。

まぁ、、、ただ実装してて思いましたが、Timer のコールバックって ThreadPool の忙しさとか諸々の影響で厳密な時間でイベントが発生するわけじゃないので(1ms程度は普通に前後するので)、実行が前後したり、逆に連続で発生したり、間隔が空くときもあるので、1ms の実行間隔は全然実用的でないと思います。

それにハンドラ内の処理も 1ms未満の処理じゃないといけないですがシビア過ぎて使いどころが難しいですよね。

逆に、timeBeginPeriod1 を指定した際に、既存のタイマー処理のインターバルに 1 が指定されていてだけどシステム解像度に依存して 15.6ms 周期なんでしょ、とかいう処理があると逆に問題が発生するので、もじ解像度を変更するとなった時の影響個所の調査時に頭の片隅に覚えておいたほうがいい事項って感じだと思いました。

最近のWindowsでは(Win10 ver2004以降)では timeBeginPeriod の影響範囲がOS全体からプロセス内に影響範囲が限定されたので以前より、timeBeginPeriod を利用はしやすくなしましたが、バカデカ既存レガシーシステムではそういった点で罠が潜んでるかもしれませんね。

参考資料

今回参考にしたページは以下の通り(MS Learn はアドレスが変わりやすいのでリンク切れの際はタイトルで検索してください)

Title: Timer Class(System.Threading.Timer)

learn.microsoft.com

Title: timeBeginPeriod function (timeapi.h)

learn.microsoft.com

関連記事

9年前に書いた記事

takap-tech.com

*1:もはやこんな古めかしいクラス使ってない?自分もそう思います。

*2:PeriodicTimer使え?それも本当にそう。