LoginSignup
0
3

More than 1 year has passed since last update.

C#マイクロ秒レベルの高精度Timer

Posted at

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;
    }
}
0
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3