QTimer 对象 运行 是否在单独的线程中?它的机制是什么?

Does a QTimer object run in a separate thread? What is its mechanism?

When I create a QTimer object in Qt 5, and start it using the start() member function, is a separate thread created that keeps track of the time and calls the timeout() function at regular intervals?

例如,

QTimer *timer = new QTimer;
timer->start(10);
connect(timer,SIGNAL(timeout()),someObject,SLOT(someFunction()));

这里,程序怎么知道什么时候发生timeout()呢?我认为它必须 运行 在一个单独的线程中,因为我看不到顺序程序如何跟踪时间并同时继续执行。但是,我无法在 Qt 文档或其他任何地方找到与此相关的任何信息来确认这一点。

我看过 the official documentation, and certain questions on Whosebug such as this and this 似乎很相关,但我无法通过它们得到答案。

谁能解释一下 QTimer 对象的工作机制?

进一步搜索,我发现根据this answer by Bill,提到

Events are delivered asynchronously by the OS, which is why it appears that there's something else going on. There is, but not in your program.

是否意味着 timeout() 由 OS 处理?是否有一些硬件可以跟踪时间并以适当的时间间隔发送中断?但如果是这样的话,那么多的定时器可以同时独立运行,那如何才能对每个定时器分别进行跟踪呢?

机制是什么?

谢谢。

查看 documentation about timers and at the source code of QTimer and QObject 我们可以看到定时器在 thread/event 循环中分配给对象的 运行。来自文档:

For QTimer to work, you must have an event loop in your application; that is, you must call QCoreApplication::exec() somewhere. Timer events will be delivered only while the event loop is running.

In multithreaded applications, you can use QTimer in any thread that has an event loop. To start an event loop from a non-GUI thread, use QThread::exec(). Qt uses the timer's thread affinity to determine which thread will emit the timeout() signal. Because of this, you must start and stop the timer in its thread; it is not possible to start a timer from another thread.

在内部,QTimer 只是使用 QObject::startTimer 方法在一定时间后触发。这个本身以某种方式告诉线程它 运行 在一段时间后触发。

因此,只要您不阻塞事件队列,您的程序就可以 运行 持续跟踪计时器。如果您担心您的计时器不是 100% 准确,请尝试将 long-running 回调移出它们自己线程中的事件队列,或者为计时器使用不同的事件队列。

When I create a QTimer object in Qt 5, and start it using the start() member function, is a separate thread created that keeps track of the time and calls the timeout() function at regular intervals?

否;创建一个单独的线程会很昂贵并且没有必要,所以这不是 QTimer 的实现方式。

Here, how does the program know when timeout() occurs?

QTimer::start() 方法可以调用系统时间函数(例如 gettimeofday() 或类似函数)以找出(在几毫秒内)调用 start() 的时间.然后它可以将 10 毫秒(或您指定的任何值)添加到该时间,现在它有一条记录指示下一次应该发出 timeout() 信号的时间。

有了这些信息,它会做什么来确保发生这种情况?

要知道的关键事实是 QTimer timeout-signal-emission 只能工作 if/when 您的 Qt 程序在 Qt 的事件循环中执行。几乎每个 Qt 程序都会有这样的东西,通常靠近它的 main() 函数的底部:

QApplication app(argc, argv);
[...]
app.exec();

请注意,在典型的应用程序中,应用程序的几乎所有时间都将花费在 exec() 调用中;也就是说,app.exec() 调用 在应用程序退出之前不会 return

那么当您的程序 运行 时,exec() 调用内部发生了什么?对于像 Qt 这样的大型复杂库,它必然很复杂,但是说它是 运行 一个概念上看起来像这样的事件循环并不过分简化:

 while(1)
 {
     SleepUntilThereIsSomethingToDo();  // not a real function name!
     DoTheThingsThatNeedDoingNow();     // this is also a name I made up
     if (timeToQuit) break;
 }

因此,当您的应用处于空闲状态时,进程将在 SleepUntilThereIsSomethingToDo() 调用中进入休眠状态,但一旦需要处理的事件到达(例如,用户移动鼠标,或按下某个键,或数据到达套接字等),SleepUntilThereIsSomethingToDo() 将 return 然后执行响应该事件的代码,从而导致适当的操作,例如小部件更新或调用 timeout() 信号.

那么 SleepUntilThereIsSomethingToDo() 是如何知道何时该醒来的 return?这将根据您 运行 使用的 OS 有很大差异,因为不同的 OS 有不同的 API 来处理这类事情,但是经典的 UNIX-y 方式实现这样的功能将使用 POSIX select() 调用:

int select(int nfds, 
           fd_set *readfds, 
           fd_set *writefds,
           fd_set *exceptfds, 
           struct timeval *timeout);

请注意,select() 接受三个不同的 fd_set 参数,每个参数都可以指定多个文件描述符;通过将适当的 fd_set 对象传递给这些参数,您可以使 select() 在您关心的一组文件描述符中的任何一个成为可能的 I/O 操作的瞬间唤醒监控,以便您的程序可以立即处理 I/O。然而,对我们来说有趣的部分是最后一个参数,它是一个 timeout-argument。特别是,您可以在此处传递一个 struct timeval 对象,它表示 select(): "If no I/O events have occurred after (this many) microseconds, then you should just give up and return anyway".

事实证明这非常有用,因为通过使用该参数,SleepUntilThereIsSomethingToDo() 函数可以执行如下操作(伪代码):

void SleepUntilThereIsSomethingToDo()
{
   struct timeval now = gettimeofday();  // get the current time
   struct timeval nextQTimerTime = [...];  // time at which we want to emit a timeout() signal, as was calculated earlier inside QTimer::start()
   struct timeval maxSleepTimeInterval = (nextQTimerTime-now);
   select([...], &maxSleepTimeInterval);  // sleep until the appointed time (or until I/O arrives, whichever comes first)
}

void DoTheThingsThatNeedDoingNow()
{
   // Is it time to emit the timeout() signal yet?
   struct timeval now = gettimeofday();
   if (now >= nextQTimerTime) emit timeout();

   [... do any other stuff that might need doing as well ...]
}   

希望这是有道理的,您可以看到事件循环如何使用 select() 的超时参数来允许它在(大约)时间唤醒并发出 timeout() 信号它之前在您调用 start() 时计算过。

顺便说一句,如果应用程序有多个 QTimer 同时处于活动状态,那没问题;在这种情况下,SleepUntilThereIsSomethingToDo() 只需要遍历所有活动的 QTimer 以找到具有最小 next-timeout-time 标记的那个,并且仅使用该最小时间戳来计算最大值 time-interval select() 应该允许睡眠。然后在 select() returns 之后,DoTheThingsThatNeedDoingNow() 也会遍历活动计时器,并仅为那些 next-timeout-time 戳记不大于当前时间的计时器发出超时信号。 event-loop 重复(根据需要快或慢)以提供多线程行为的假象,而实际上不需要多线程。

QTimer 对象将自身注册到 EventDispatcher (QAbstractEventDispatcher) 中,然后每次特定注册的 QTimer 超时时都会发送 QTimerEvent 类型的事件。例如,在 GNU/Linux 上有一个名为 QEventDispatcherUNIXPrivate 的 QAbstractEventDispatcher 私有实现,它在计算时考虑了平台 api。 QTimerEvent 从 QEventDispatcherUNIXPrivate 发送到 QTimer 对象所属的同一线程的事件循环队列中,即创建。

QEventDispatcherUNIXPrivate 不会因为某些 OS 系统事件或时钟而触发 QTimerEvent,而是因为当 QTimer 所在的线程事件循环调用 processEvents 时它会定期检查超时。在这里:https://code.woboq.org/qt5/qtbase/src/corelib/kernel/qeventdispatcher_unix.cpp.html#_ZN27QEventDispatcherUNIXPrivateC1Ev