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

36
当我在Qt 5中创建一个QTimer对象,并使用start()成员函数启动它时,是否会创建一个单独的线程来跟踪时间并定期调用timeout()函数?
例如:
QTimer *timer = new QTimer;
timer->start(10);
connect(timer,SIGNAL(timeout()),someObject,SLOT(someFunction()));

在这里,程序如何知道何时发生timeout()?我认为它必须在一个单独的线程中运行,因为我不知道一个顺序程序如何同时跟踪时间并继续执行。然而,我无法在Qt文档或其他任何地方找到任何关于此的信息以确认此事。
我已经阅读了官方文档,以及一些StackOverflow上的问题,例如这个这个,但是我无法通过它们得到答案。
有人可以解释一下QTimer对象的工作机制吗?
进一步搜索后,我发现根据Bill这个答案,提到:
事件由操作系统异步传递,这就是为什么似乎有其他事情正在发生。确实如此,但不是在您的程序中。

这是否意味着timeout()由操作系统处理?是否有一些硬件跟踪时间并在适当的间隔发送中断?但如果是这种情况,由于许多计时器可以同时独立运行,每个计时器如何被单独跟踪?

机制是什么?

谢谢。
3个回答

39
当我在Qt 5中创建一个QTimer对象,并使用start()成员函数启动它时,会创建一个单独的线程来跟踪时间并定期调用timeout()函数吗?
不会;创建一个单独的线程会很昂贵且不必要,因此QTimer没有这样实现。
那么程序如何知道何时发生timeout()呢?
QTimer::start()方法可以调用系统时间函数(例如gettimeofday()或类似函数),以了解(在几毫秒内)调用start()时的时间。然后,它可以将十毫秒(或您指定的任何值)添加到该时间,现在它有一个记录,指示下一次应发射timeout()信号的时间。
那么,有了这些信息,它会采取什么措施确保发生呢?
要知道的关键事实是,只有当你的Qt程序在Qt的事件循环中执行时,QTimer timeout信号才能被发射。几乎每个Qt程序都会有类似于此的东西,通常位于其main()函数的底部:
QApplication app(argc, argv);
[...]
app.exec();

请注意,在典型的应用程序中,几乎所有的时间都将花费在exec()调用内部;也就是说,app.exec()调用直到应用程序退出时才会返回。
那么,在程序运行时,exec()调用内部发生了什么?对于像Qt这样的大型复杂库来说,它必然很复杂,但可以简单地说,它正在运行一个事件循环,概念上类似于以下内容:
 while(1)
 {
     SleepUntilThereIsSomethingToDo();  // not a real function name!
     DoTheThingsThatNeedDoingNow();     // this is also a name I made up
     if (timeToQuit) break;
 }

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

那么SleepUntilThereIsSomethingToDo()如何知道何时唤醒并返回呢?这将根据您运行的操作系统而大不相同,因为不同的操作系统有不同的API来处理此类事情,但实现这样一个函数的经典UNIX方式是使用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。然而,对我们来说有趣的部分是最后一个参数,它是一个超时参数。特别是,您可以在这里传递一个struct timeval对象,该对象告诉select():“如果在这么多微秒后没有发生I/O事件,那么你应该放弃并返回”。
这非常有用,因为通过使用该参数,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()的超时参数,以允许它在调用start()时预先计算的时间(大约)唤醒并发出timeout()信号。
顺便说一下,如果应用程序同时具有多个活动的QTimer,那就没有问题;在这种情况下,SleepUntilThereIsSomethingToDo()只需要遍历所有活动的QTimer,找到具有最小下一次超时时间戳的QTimer,并仅使用该最小时间戳来计算select()应该允许睡眠的最大时间间隔。然后,在select()返回后,DoTheThingsThatNeedDoingNow()也遍历活动的计时器,并仅为那些下一个超时时间戳不大于当前时间的计时器发出超时信号。事件循环重复进行(尽可能快或尽可能慢),以呈现类似于多线程行为的外观,而不实际需要多个线程。

1
非常感谢!那真的很有帮助。我有一个问题 - 在函数SleepUntilThereIsSomethingToDo()中,如果需要处理某些输入,则函数会提前返回。如果if (now >= nextQTimerTime)还不成立,则在DoTheThingsThatNeedDoingNow()中不会发出超时信号。稍后,在回到SleepUntilThereIsSomethingToDo()时,计时器将被重置。所以这里不是跳过了一个timeout()吗?或者我误解了什么?谢谢。 - GoodDeeds
1
每次调用SleepUntil()时,超时间隔都会重新计算,但定时器不会被重置,超时也不会被跳过。例如:假设在下午2点您调用QTimer::start()并将QTimer的间隔设置为1小时。在下一次调用SleepUntil()时,超时时间被计算为(3PM-2PM = 1小时)。但是在下午2:30有一些输入进来,所以select()返回并处理了输入。现在再次调用SleepUntil(),并计算新的超时时间为(3PM-2:30PM=30分钟)。因此(假设没有进一步的输入),它仍然会在下午3点唤醒并发出信号。 - Jeremy Friesner
1
是的,很可能每个QTimer都会在QTimer对象内部的某个私有变量中存储自己的nextQTimerTime,并且事件循环会查看所有的QTimer对象,以找到其中最小的nextQTimerTime值,并使用它。 (可能通过使用优先队列或类似的数据结构进一步优化,但我没有检查以确保) - Jeremy Friesner
如果超时槽函数和同一类中的其他槽函数具有共享数据,那么如果这些函数仅通过信号调用,则它们不需要同步?这意味着,当信号发生时执行第一个槽的线程将按顺序继续执行下一个槽吗? - k_kaz
1
@k_kaz 没错 - 它们默认情况下都在同一个线程中运行,因此除非您明确将 QTimer 对象移动到其他线程,否则可以保证槽调用会按顺序发生,因此您不需要进行任何同步。 - Jeremy Friesner
显示剩余2条评论

6

查看关于计时器的文档以及QTimerQObject的源代码,我们可以看到计时器在分配给对象的线程/事件循环中运行。从文档中可以得知:

为使QTimer工作,您必须在应用程序中有一个事件循环;也就是说,您必须在某个地方调用QCoreApplication::exec()。只有在事件循环正在运行时,定时器事件才会被传递。

在多线程应用程序中,您可以在拥有事件循环的任何线程中使用QTimer。要从非GUI线程启动事件循环,请使用QThread::exec()。Qt使用计时器的线程亲和性来确定哪个线程将发出timeout()信号。因此,您必须在其线程中启动和停止计时器;无法从另一个线程启动计时器。

在内部,QTimer 简单地使用 QObject::startTimer 方法在一定时间后触发。这个方法会告诉线程在指定的时间之后触发。

因此,只要不阻塞事件队列,你的程序就可以持续运行并跟踪计时器。如果你担心计时器不准确,请将长时间运行的回调移出事件队列,在它们自己的线程中运行,或者为计时器使用不同的事件队列。


谢谢。但是程序的“控制流”如何在单个线程中执行两个操作 - 顺序执行语句以及跟踪时间?其次,事件循环在其中扮演什么角色? - GoodDeeds
@GoodDeeds 关于您的流程控制问题,您需要告诉我们您的应用程序正在做什么。但我无法告诉您计时器本身是如何实现的 - 我可以想到a)它们正在比较当前时间和应该触发的时间或b)使用一些硬件/操作系统中断功能。 - msrd0
谢谢。我发现了这个答案https://dev59.com/VknSa4cB1Zd3GeqPOYDP#1472579(也添加到我的问题中),它似乎为注释的第二部分提供了一些见解 - 看起来(b)是正确的。但是真的吗?此外,如果它像(a)中所述那样工作,那么岂不是必须有一些线程定期检查时间吗?这不会导致不精确吗? - GoodDeeds
@GoodDeeds 如果你记得上一次拍摄的时间并将其与下一次拍摄的时间进行比较,你会发现存在许多不准确之处,我认为没有简单的方法可以摆脱它们。但平均值相当接近你想要的结果,大多数应用程序在精度方面都可以被称为相对准确的间隔。要获得最高的准确性,您需要查看处理超时的低级函数。 - msrd0

6
QTimer对象将自己注册到EventDispatcher(QAbstractEventDispatcher)中,然后由它负责在特定的已注册QTimer超时时发送QTimerEvent类型的事件。例如,在GNU/Linux上,有一个名为QEventDispatcherUNIXPrivate的QAbstractEventDispatcher的私有实现,该实现考虑了平台API的时间计算。 QTimerEvent从QEventDispatcherUNIXPrivate发送到同一线程的事件循环队列中,即QTimer对象所属的线程中,也就是创建它的线程。
QEventDispatcherUNIXPrivate不是因为某些操作系统系统事件或时钟而触发QTimerEvent的,而是因为它在由QTimer所属的线程事件循环调用processEvents时定期检查超时。详见此处:https://code.woboq.org/qt5/qtbase/src/corelib/kernel/qeventdispatcher_unix.cpp.html#_ZN27QEventDispatcherUNIXPrivateC1Ev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接