C#にはMS側で用意されたいくつかTimerクラスがありますが、どれもミリ秒でバラつきが発生する精度です。
ミリ秒、マイクロ秒レベルで正確、かつCPU負荷を減らした高精度Timerを後述します。
既存Timerの精度問題
既存のTimerでは実行間隔に5~30msほどのバラつきがあります。
2022/08/02 12:26:36.0071
2022/08/02 12:26:37.0129
2022/08/02 12:26:38.0171
2022/08/02 12:26:39.0160
2022/08/02 12:26:40.0206
2022/08/02 12:26:41.0060
2022/08/02 12:26:42.0099
2022/08/02 12:26:43.0162
2022/08/02 12:26:44.0195
2022/08/02 12:26:45.0070
2022/08/02 12:26:46.0065
2022/08/02 12:26:47.0062
2022/08/02 12:26:48.0081
2022/08/02 12:26:49.0097
2022/08/02 12:26:50.0142
2022/08/02 12:26:51.0198
2022/08/02 12:26:52.0101
2022/08/02 12:26:53.0168
2022/08/02 12:26:54.0171
2022/08/02 12:26:55.0040
2022/08/02 12:26:56.0087
高精度Timerクラス
後述するTimerクラスでは初回のみマイクロ秒ブレますが、それ以外は正確になっています。
最小間隔は16msです。
2022/08/02 12:21:43.0003
2022/08/02 12:21:44.0000
2022/08/02 12:21:45.0000
2022/08/02 12:21:46.0000
2022/08/02 12:21:47.0000
2022/08/02 12:21:48.0000
2022/08/02 12:21:49.0000
2022/08/02 12:21:50.0000
2022/08/02 12:21:51.0000
2022/08/02 12:21:52.0000
2022/08/02 12:21:53.0000
2022/08/02 12:21:54.0000
2022/08/02 12:21:55.0000
2022/08/02 12:21:56.0000
2022/08/02 12:21:57.0000
2022/08/02 12:21:58.0000
2022/08/02 12:21:59.0000
2022/08/02 12:22:00.0000
2022/08/02 12:22:01.0000
2022/08/02 12:22:02.0000
2022/08/02 12:22:03.0000
2022/08/02 12:22:04.0000
実装
public class HighResolutionTimer : IDisposable
{
private const double MIN_INTERVAL_MILLISECONDS = 16;
public event EventHandler? Elapsed;
private TimeSpan _interval;
private TimeSpan Interval
{
get
{
return _interval;
}
set
{
if (value.TotalMilliseconds < MIN_INTERVAL_MILLISECONDS)
throw new Exception($"{MIN_INTERVAL_MILLISECONDS}ms以下を設定できません。");
_interval = value;
}
}
private volatile bool _running = false;
public HighResolutionTimer(TimeSpan interval)
{
Interval = interval;
}
public void Start(DateTime startAt)
{
running = true;
Task.Run(() =>
{
var nextTriggerAt = startAt;
while (_running)
{
while (true)
{
var rest = (nextTriggerAt - DateTime.UtcNow).TotalMilliseconds;
// Sleepメソッドには16msまでの精度しかないため、16msまではSleepで待機し、
// それ以降はSpinWaitでCPU負荷を下げる
if (rest > MIN_INTERVAL_MILLISECONDS)
Thread.Sleep((int)(rest - MIN_INTERVAL_MILLISECONDS));
else if (rest > 0)
Thread.SpinWait(50);
else
break;
}
Elapsed?.Invoke(this, new EventArgs());
nextTriggerAt = nextTriggerAt.AddMilliseconds(Interval.TotalMilliseconds);
}
});
}
public void Stop()
{
_running = false;
}
public void Change(TimeSpan interval, DateTime startAt)
{
Stop();
Interval = interval;
Start(startAt);
}
public void Dispose()
{
Stop();
Elapsed = null;
}
}