是什么让我的计时器跑得不准确

本文关键字:不准确 计时器 我的 是什么 | 更新日期: 2023-09-27 18:03:07

我有一个类,它创建一个新的计时器,并在每个滴答时轮询一个远程队列(通过HTTP)。

public void Start()
{
    _timer = new Timer((x) =>
    {
        Console.WriteLine(DateTime.Now.ToString("hh:MM:ss.fff") + " " + typeof(T).Name);                
        var message = (Message)null;
        var messageBody = (T)null;
        try
        {
            if (!_queue.TryGet(out message))
                return;
                messageBody = (T)JsonConvert.DeserializeObject(message.Body, typeof(T));
                _messageDispatcher.Dispatch<T>(messageBody);
                _queue.Delete(message.Id);
            }
            catch (Exception ex)
            {
                _errorHandler.Handle(ex, message);
            }                
        }, null, 0, _queueConsumerConfiguration.PollingInterval);            
    }
}

如果我创建了该类的8个新实例,将轮询间隔设置为250ms,并调用它们,我可以假设计时器会非常准确地滴答。在计时器回调中执行什么并不重要。然而,事实并非如此。

01:03:23.305 MessageSleepForOneSecond
01:03:23.301 MessageSleepForOneSecond
01:03:23.297 MessageSleepForOneSecond
01:03:23.316 MessageSleepForOneSecond
01:03:24.321 MessageSleepForOneSecond
01:03:24.562 MessageSleepForOneSecond
01:03:24.701 MessageSleepForOneSecond
01:03:24.707 MessageSleepForOneSecond
01:03:24.716 MessageSleepForOneSecond
01:03:25.321 MessageSleepForOneSecond
01:03:25.518 MessageSleepForOneSecond
01:03:25.764 MessageSleepForOneSecond
01:03:25.912 MessageSleepForOneSecond
01:03:25.920 MessageSleepForOneSecond
01:03:25.924 MessageSleepForOneSecond
01:03:26.521 MessageSleepForOneSecond
01:03:26.710 MessageSleepForOneSecond
01:03:26.957 MessageSleepForOneSecond
01:03:27.107 MessageSleepForOneSecond
01:03:27.120 MessageSleepForOneSecond
01:03:27.126 MessageSleepForOneSecond
01:03:27.716 MessageSleepForOneSecond
01:03:27.906 MessageSleepForOneSecond
01:03:28.151 MessageSleepForOneSecond
01:03:28.305 MessageSleepForOneSecond
01:03:28.316 MessageSleepForOneSecond
01:03:28.322 MessageSleepForOneSecond
01:03:28.913 MessageSleepForOneSecond
01:03:29.100 MessageSleepForOneSecond
01:03:29.349 MessageSleepForOneSecond
01:03:29.502 MessageSleepForOneSecond
01:03:29.513 MessageSleepForOneSecond
01:03:29.538 MessageSleepForOneSecond
01:03:30.107 MessageSleepForOneSecond
01:03:30.297 MessageSleepForOneSecond
01:03:30.545 MessageSleepForOneSecond
01:03:30.705 MessageSleepForOneSecond
01:03:30.712 MessageSleepForOneSecond
01:03:30.733 MessageSleepForOneSecond
01:03:31.307 MessageSleepForOneSecond
01:03:31.310 MessageSleepForOneSecond
...

怎么回事?是什么导致了不准确?管理线程池,Windows,…?

是什么让我的计时器跑得不准确

计时器不是无限精确的,线程调度也不是无限精确的。它们中的任何一个都具有最高1ms(通过timeBeginPeriod配置)和当前版本Windows默认16.7ms(在15年以上的Windows上约为50ms)的精度。

计时器本身不是无限精确的,它生成一个事件,使线程池中的线程准备就绪,该事件将在以后的任何时间被调度,不精确,也没有任何硬保证。如果其他线程正在运行,那么根据处理器负载和线程优先级,"任何时间之后"都可以任何时间之后(甚至永远不会)。

这已经足够解释为什么通常你选择的时间间隔都不准确(除非是巧合),但是250ms的时间间隔就不准确了(因为249ms是调度程序默认粒度的最接近的整数倍,所以一个新线程最多可以在265.6ms之后调度——这是假设在那个精确的时间有一个核心可用,这是不保证的)。

此外,处理HTTP请求可能会花费相当可观的时间,因此即使在没有其他线程的情况下,也很有可能创建的工作线程多于内核。这必然意味着操作系统必须决定在特定时间安排哪个操作系统。无论调度器做什么,最终总会有一个或几个线程出现"不公平"的情况,导致它们"不准确"。

对控制台的写入也是如此。对控制台的写入是同步的(即,当两个线程向控制台写入时,单个写入可能以任何顺序出现,但它们不会乱码)。这必然意味着有一把锁。谁先拿到锁,谁就继续。任何试图在一纳秒后拿到锁的人都会被阻止。其他线程接管了时间片。最终,被阻塞的线程将再次"就绪",但不能严格保证该线程将立即运行。

准备跑步和跑步是完全不同的两件事。