当我在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();
DoTheThingsThatNeedDoingNow();
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();
struct timeval nextQTimerTime = [...];
struct timeval maxSleepTimeInterval = (nextQTimerTime-now);
select([...], &maxSleepTimeInterval);
}
void DoTheThingsThatNeedDoingNow()
{
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()也遍历活动的计时器,并仅为那些下一个超时时间戳不大于当前时间的计时器发出超时信号。事件循环重复进行(尽可能快或尽可能慢),以呈现类似于多线程行为的外观,而不实际需要多个线程。
SleepUntilThereIsSomethingToDo()
中,如果需要处理某些输入,则函数会提前返回。如果if (now >= nextQTimerTime)
还不成立,则在DoTheThingsThatNeedDoingNow()
中不会发出超时信号。稍后,在回到SleepUntilThereIsSomethingToDo()
时,计时器将被重置。所以这里不是跳过了一个timeout()
吗?或者我误解了什么?谢谢。 - GoodDeeds