システム時刻を変更したときの.Netプログラムの時間関係への影響

サーバー系のプログラムでは一定時間ごとに処理を行う定期処理を組む場合がある。もし動作中にOSのシステム時刻を変更された場合にどうなるのか。今回は.NetでのWindowsのシステム時刻を変更したときの動作を確認した。

SleepとDelay

まずはSleep系の挙動を確認する。恐らくクロックカウントを利用しているのでシステム時刻の影響は無いと想定される。

Thread.SleepとTask.Delayで次のようなコードを書いた。なお環境は.Net Core3.1だ。

void TestSleep() {
    while (true) {
        Thread.Sleep(10000);
        Debug.WriteLine("tick");
    }
}
async Task TestDelay() {
    while (true) {
        await Task.Delay(10000);
        Debug.WriteLine("tick");
    }
}

プログラムを起動した状態でシステム時刻を適当にずらす。ストップウォッチを持って計測したところ、システム時刻を進めても戻してもきちんと10秒ごとにデバッグ文が出力された。

Thread.SleepとTask.Delayはシステム時刻に影響を受けないことが確認できた。

DateTime.Now

次にDateTimeの現在日時を確認する。Sleepが定期動作することが分かったので次のコードを書く。

void TestDateTime() {
    while (true) {
        Thread.Sleep(10000);
        Debug.WriteLine($"{DateTime.Now:MM/dd HH:mm:ss}");
    }
}
結果
11/12 21:40:33
11/12 21:40:43 _ここで5秒時間を戻す
11/12 21:40:48
11/12 21:40:58

当然ではあるがシステム時刻に合わせて日時が変わった。このことからDateTime.Nowを経過時間の判定に利用する場合は、時刻飛びや遡りが起きることに注意が必要だ。

Stopwatch

次は高性能タイマーとして提供されているStopwatchクラスだ。

void TestStopWatch() {
    var sw = new Stopwatch();
    sw.Start();
    int count = 0;

    while (true) {
        Thread.Sleep(10000);
        Debug.WriteLine($"{++count}:{sw.ElapsedMilliseconds / 1000}sec");
    }
}
結果
1:10sec
2:20sec
3:30sec
4:40sec
5:50sec

動作中に適当にシステム時刻を進めたり戻したりしたが、Stopwatchの経過時間の値には影響はなく一定だった。

Environment.TickCount

次にマシン起動時からの経過時間を示すEnvironment.TickCount64を確認する。

void TestTickCount() {
    long start = Environment.TickCount64;
    int count = 0;

    while (true) {
        Thread.Sleep(10000);
        Debug.WriteLine($"{++count}:{(Environment.TickCount64 - start) / 1000}sec");
    }
}
結果
1:10sec
2:20sec
3:30sec
4:40sec
5:50sec

こちらもStopwatchと同じくシステム時刻の影響を受けず、値が遡ることも無かった。

なお64のつかない「TickCount」は24.9日でカウントが一周してしまう。長期稼働する場合は注意が必要だ。

Windows Form Timer

今度はWindows Formで提供されるTimerコントロールの挙動を確認する。プロジェクトを.Net Frameowrk4.7.2のWindows Formに切り替える。

void TestFormTimer() {
    var start = Environment.TickCount;
    var timer = new Timer();
    int count = 0;

    timer.Interval = 10000;
    timer.Tick += (sender, args) => {
        Debug.WriteLine($"{++count}:{(Environment.TickCount - start) / 1000}sec");
    };
    timer.Start();
}
結果
1:10sec
2:20sec
3:30sec
4:40sec
5:50sec

WindowsFormTimerもシステム日時の変更の影響は受けなかった。時刻を戻したときにイベント発火が遅れたり、進めたらすぐに発火したりはしなかった。

DispatcherTimer

次は主にWPFアプリで利用されるDispatcherTimerだ。WPFプロジェクトに切り替える。バージョンは.Net Core3.1。

void TestDispatcherTimer() {
    var start = Environment.TickCount64;
    var timer = new DispatcherTimer();
    int count = 0;

    timer.Interval = new TimeSpan(0,0,10);
    timer.Tick += (sender, args) => {
        Debug.WriteLine($"{++count}:{(Environment.TickCount64 - start) / 1000}sec");
    };
    timer.Start();
}
結果
1:10sec
2:20sec
3:30sec
4:40sec
5:50sec

こちらもForm Timerと同じでシステム時刻を変更しても発生間隔は変わらなかった。

まとめ

デスクトップアプリでFormTimerやDispatcherTimerを利用している場合は、システム時刻を変更しても一定間隔が保たれる。

ループでの定期処理やタイムアウト処理の実装で経過時間を測るなら、DateTime.NowよりStopwatchやEnvironment.CurrentTime64を利用した方が時刻の変更による影響に強くなる。

コメント