QTimer::singleShot(0, object SLOT(obj_slot()))是什么意思?

9

我是一个初学者,正在学习Qt,并尝试理解Qt提供的下载操作示例。在downloadmanager.cpp中,一个成员函数如下:

void DownloadManager::append(const QUrl &url)
{
    if (downloadQueue.isEmpty())
        QTimer::singleShot(0, this, SLOT(startNextDownload()));

    downloadQueue.enqueue(url);
    ++totalCount;
}
  • 如果downloadQueue为空,为什么需要在添加url之前激活startNextDownload()呢?(请注意:startNextDownload()会在downloadQueue为空时结束程序)
  • 我不确定为什么要使用QTimer::singleShot(x, y, z),因为据我所知,它是一个延迟0毫秒激活槽的定时器。
  • 我无法从Qt Assistant中确定singleShot是针对给定毫秒间隔重复激活插槽的一次性设置还是一次性的。

澄清:

我是个初学者,在像这样的示例中:

statement1;
statement2;

我习惯于在继续处理statement2之前先运行并完成statement1。但是尝试学习Qt并阅读给定的示例后,我发现SLOT(startNextDownload())downloadQueue.enqueue(url);已经执行后被激活。我正在努力理解为什么这样可以工作。


QT4是有意使用的吗?难道你们不用QT5吗? - realvictorprm
我正在使用Qt 4.8.7。 - Robert C. Holland
好的,只是想知道因为你是Qt的初学者。我以为通常初学者会使用最新版本 :) - realvictorprm
1
谢谢你的问题,具有讽刺意味的是,它帮助我更好地理解了这个东西。 - realvictorprm
3个回答

4
这会将一个回调函数放入消息队列中。
定时器立即到期,并向消息队列发布一条消息。当进程下次到达主循环时,将调用startNextDownload()函数。此时,URL已在队列中。 startNextDownload()函数从分派上下文中调用,在这里更改窗口内容是安全的。这样,DownloadManager类可以用于多线程应用程序,其中启动下载的线程可能与Paint事件的处理程序同时运行。通过从处理Paint事件的同一线程调用它,您可以确保没有这样的事件正在被处理,并且可以安全地更新小部件。
如果小部件之后需要重新绘制,则请求重新绘制,并且如果小部件当前可见,则操作系统将发送Paint事件。

谢谢,这让我有点明白了。由于我没有做过任何线程编程,我理解它是将其推送到要执行的任务队列中,稍后将逐个完成。但是为什么SLOT(startNextDownload())确保在将URL添加到downloadQueue之后运行(即使该slot出现/被推入添加之前的任务队列)?我也不明白:什么是主循环,是什么导致进程达到主循环?但我不知道“当进程下一次到达主循环时”是什么意思:什么是主循环,是什么导致它到达主循环? - Robert C. Holland
@RobertC.Holland,等一下...你是对的,在多线程应用程序中,这可能会遇到问题。我会重写为bool const needToStart = downloadQueue.isEmpty(); downloadQueue.enqueue(url); if(needToStart) QTimer::singleShot(...); - Simon Richter
1
@RobertC.Holland,主循环是指程序在空闲状态下所处的位置。它是一个无限循环,在循环中它会从队列中取出第一个事件并处理,如果没有剩余事件,则会请求操作系统给其他进程分配CPU时间,或者将CPU关闭直到发生有趣的事情。GUI程序通常由一系列事件处理程序组成,当它们返回时,程序会回到主循环。 - Simon Richter
当一个小部件更新被调度时,操作系统并不会发送重绘事件。这样做是浪费的,因为事件循环拥有调用重绘所需的所有信息 - 它不需要等待本地事件队列。当您使用 QWidget::update 时,操作系统不会发送任何重绘事件。在某些平台上,操作系统可能会将顶层窗口更新与垂直同步进行同步,但 Qt 本身将提前执行后备位图的更新,并且由垂直同步触发的操作只会将位图复制到屏幕上。 - Kuba hasn't forgotten Monica
@KubaOber,这些是实现细节,会使答案变得比必要的复杂——行为就像计时器立即过期一样。同样地,如果在当前平台上安全执行,则Qt可以优化更新小部件的路径,但某些系统甚至没有后备位图,因此渲染目标仅在绘制事件期间可用。此外,如果您从主循环外部修改UI状态,则需要额外的锁定来将您的更新与其他线程同步。 - Simon Richter
显示剩余3条评论

2

当前问题的回答

每次调用 QTimer::singleShot(...) 都会在其所在的线程的事件循环中执行 **。如果是从主线程调用,则是由 app.exec() 启动的事件循环。

根据 Qt-Network-Manager-Example,此函数在网络管理器填充了 URL 后被调用,因此单个定时器将在队列完全填充后进行处理。可惜的是,Qt 文档对这个话题的说明还不够清晰,因此有关事件处理等更多信息,请参见此处


旧问题的回答

在开始之前,定时器是为了将下载放在一个额外的线程中,以便 GUI 保持响应。

完整的 downloadNext() 方法是递归的。它只会被调用一次,并在队列为空之前一直被调用。请看这个:

void DownloadManager::append(const QStringList &urlList)
{
    foreach (QString url, urlList)
        append(QUrl::fromEncoded(url.toLocal8Bit())); //Call for only one URL
  ...
}

void DownloadManager::append(const QUrl &url)
{
    if (downloadQueue.isEmpty())
        //I'm only called if the queue is empty! And I will be called after the next line. Not instantly!
        QTimer::singleShot(0, this, SLOT(startNextDownload()));  

    downloadQueue.enqueue(url);
    ++totalCount;
}

当队列为空时,每个方法都会返回,至少会打印出下载完成的消息。

那么为什么这能够工作呢? 请参阅下面的第一章节。


谢谢提供详细信息,但是QTimer::singleShot(0, this, SLOT(startNextDownload()));作为一个单独的线程,如何保证在downloadQueue.enqueue(url);发生之前或之后运行(考虑到只有一个主线程)? - Robert C. Holland
因为它在Qt事件线程上运行,该线程在调用app.exec()后开始执行。 - realvictorprm
1
我不确定这是否正确。据我所知,插槽将在所属的任何线程上运行。这不一定是主UI线程(根据文档,函数将在接收器的线程中运行)。因此,当前所述的答案是完全错误的。 - EFraim
请参考文档。 - realvictorprm
@ArthurP.R.:请参考这篇博客文章http://blog.bbv.ch/2012/10/03/multithreaded-programming-with-qt/,了解Qt中线程亲和力的实现方式。简而言之,每个QObject都有线程亲和力,排队的信号连接会自动在正确的线程中执行。 - EFraim
@EFraim 是正确的,执行被安排到拥有该槽的 QThread 的事件循环中,而 app.exec() 则启动主线程的事件循环,通常是不同的线程。 - Jeronimo

-2

抱歉,但是这并没有正确回答问题。 - realvictorprm

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