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

新手上路,请多包涵

当我在 Qt 5 中创建 QTimer 对象,并使用 start() 成员函数启动它时,创建了一个单独的线程来跟踪时间并调用 timeout() 定期运行?

例如,

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

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

我已经阅读 了官方文档,并且 StackOverflow 上的某些问题,例如 thisthis 似乎非常相关,但我无法通过它们得到答案。

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

在进一步搜索中,我发现根据 Bill这个答案,提到了

事件由操作系统异步传递,这就是为什么看起来还有其他事情发生的原因。有,但不在您的程序中。

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

机制是什么?

谢谢你。

原文由 GoodDeeds 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 4.5k
2 个回答

当我在 Qt 5 中创建 QTimer 对象并使用 start() 成员函数启动它时,是否创建了一个单独的线程来跟踪时间并定期调用 timeout() 函数?

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

在这里,程序如何知道 timeout() 何时发生?

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

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

要知道的关键事实是,QTimer timeout-signal-emission 仅在您的 Qt 程序在 Qt 的事件循环内执行时才有效。几乎每个 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-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 无延迟。然而,对我们来说有趣的部分是最后一个参数,它是一个超时参数。特别是,您可以在此处传入一个 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() 的 timeout 参数来允许它唤醒并在(大约)它之前计算的时间发出 timeout() 信号调用 start( )。

顺便说一句,如果应用程序同时有多个 QTimer 处于活动状态,那没问题;在这种情况下,SleepUntilThereIsSomethingToDo() 只需要遍历所有活动的 QTimer 以找到具有最小下一次超时时间戳的那个,并仅使用该最小时间戳来计算 select() 的最大时间间隔应该允许睡觉了。然后在 select() 返回后,DoTheThingsThatNeedDoingNow() 还会遍历活动计时器,并仅针对那些下一个超时时间戳不大于当前时间的计时器发出超时信号。事件循环重复(根据需要快速或慢速)以提供多线程行为的外观,而实际上不需要多个线程。

原文由 Jeremy Friesner 发布,翻译遵循 CC BY-SA 3.0 许可协议

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

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

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

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

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

原文由 msrd0 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题