【问题标题】:What happens with Qt signals when the receiver is busy?当接收器忙时 Qt 信号会发生什么?
【发布时间】:2013-09-11 18:05:23
【问题描述】:

在我的应用程序中,我有一个QTimer 的实例,它的timeout() 信号连接到主窗口对象中的一个槽,导致它被定期调用。插槽用相机拍照并保存到磁盘。

我想知道如果在接收器(主线程上的窗口对象)当前正忙(例如获取和保存前一个)时发出信号(从执行 QTimer 的单独线程,我假设)会发生什么图片)。上一个呼叫终止后,呼叫是否排队并执行?整个想法是让它定期运行,但是这些调用是否可以排队,然后在控制返回事件循环时随机调用,从而造成混乱?我怎样才能避免它?理论上,插槽应该快速执行,但假设硬件有问题并且出现了停顿。

我希望在这种情况下放弃呼叫而不是排队,更有用的是能够在发生这种情况时做出反应(警告用户,终止执行)。

【问题讨论】:

  • 我的直觉反应是考虑将拍摄图片并保存到磁盘的逻辑从主 gui 线程中移出到一个单独的线程中。这应该使照片线程更容易定期运行,方法是让它在照片保存后的剩余间隔时间内设置自己的计时器。
  • @Digikata:实际上,它的安排方式是 GUI 线程拥有计时器并有一个定期调用的插槽,但该插槽所做的只是为另一个负责处理的线程发出信号捕获和保存的东西。
  • 如果你想获得稳定的间隔时间,那会有一个小问题——我会在答案中展开......

标签: c++ multithreading qt qt-signals qtcore


【解决方案1】:

经过一些实验,我在这里详细介绍了QTimer 在接收方忙碌时的行为。

这里是实验源代码:(在项目文件中添加QT += testlib

#include <QtGui>
#include <QtDebug>
#include <QTest>

struct MyWidget: public QWidget
{
    QList<int> n;    // n[i] controls how much time the i-th execution takes
    QElapsedTimer t; // measure how much time has past since we launch the app

    MyWidget()
    {
        // The normal execution time is 200ms
        for(int k=0; k<100; k++) n << 200; 

        // Manually add stalls to see how it behaves
        n[2] = 900; // stall less than the timer interval

        // Start the elapsed timer and set a 1-sec timer
        t.start();
        startTimer(1000); // set a 1-sec timer
    } 

    void timerEvent(QTimerEvent *)
    {
        static int i = 0; i++;

        qDebug() << "entering:" << t.elapsed();
        qDebug() << "sleeping:" << n[i]; QTest::qSleep(n[i]);
        qDebug() << "leaving: " << t.elapsed() << "\n";
    }   
};  

int main(int argc, char ** argv)
{
    QApplication app(argc, argv);   
    MyWidget w;
    w.show();
    return app.exec();
}

当执行时间小于时间间隔时

然后正如预期的那样,计时器每秒钟稳定地运行一次。它确实考虑了执行花费了多少时间,然后方法timerEvent总是以1000ms的倍数开始:

entering: 1000 
sleeping: 200 
leaving:  1201 

entering: 2000 
sleeping: 900 
leaving:  2901 

entering: 3000 
sleeping: 200 
leaving:  3201 

entering: 4000 
sleeping: 200 
leaving:  4201 

由于接收方忙而错过一次点击时

n[2] = 1500; // small stall (longer than 1sec, but less than 2sec)

那么,在停顿结束后马上调用下一个槽,但后面的调用仍然是1000ms的倍数

entering: 1000 
sleeping: 200 
leaving:  1200 

entering: 2000 
sleeping: 1500 
leaving:  3500 // one timer click is missed (3500 > 3000)

entering: 3500 // hence, the following execution happens right away
sleeping: 200 
leaving:  3700 // no timer click is missed (3700 < 4000)

entering: 4000 // normal execution times can resume
sleeping: 200 
leaving:  4200 

entering: 5000 
sleeping: 200 
leaving:  5200 

如果由于时间的累积,也错过了下面的点击也可以,只要每次执行时只错过一次点击

n[2] = 1450; // small stall 
n[3] = 1450; // small stall 

输出:

entering: 1000 
sleeping: 200 
leaving:  1201 

entering: 2000 
sleeping: 1450 
leaving:  3451 // one timer click is missed (3451 > 3000)

entering: 3451 // hence, the following execution happens right away
sleeping: 1450 
leaving:  4901 // one timer click is missed (4901 > 4000)

entering: 4902 // hence, the following execution happens right away
sleeping: 200 
leaving:  5101 // one timer click is missed (5101 > 5000)

entering: 5101 // hence, the following execution happens right away
sleeping: 200 
leaving:  5302 // no timer click is missed (5302 < 6000)

entering: 6000 // normal execution times can resume
sleeping: 200 
leaving:  6201 

entering: 7000 
sleeping: 200 
leaving:  7201 

由于接收方很忙而错过了多次点击

n[2] = 2500; // big stall (more than 2sec)

如果错过两次或多次点击,只会出现问题。执行时间不与第一次执行同步,而是与停止完成的那一刻同步:

entering: 1000 
sleeping: 200 
leaving:  1200 

entering: 2000 
sleeping: 2500 
leaving:  4500 // two timer clicks are missed (3000 and 4000)

entering: 4500 // hence, the following execution happens right away
sleeping: 200 
leaving:  4701 

entering: 5500 // and further execution are also affected...
sleeping: 200 
leaving:  5702 

entering: 6501 
sleeping: 200 
leaving:  6702 

结论

必须使用Digikata 的解决方案如果停顿时间可能长于定时器间隔的两倍,否则就不需要了,上面的简单实现效果很好。如果您希望有以下行为:

entering: 1000 
sleeping: 200 
leaving:  1200 

entering: 2000 
sleeping: 1500 
leaving:  3500 // one timer click is missed 

entering: 4000 // I don't want t execute the 3th execution
sleeping: 200 
leaving:  4200 

然后你仍然可以使用简单的实现,只需检查enteringTime &lt; expectedTime + epsilon。如果是真的,拍照,如果是假的,什么都不做。

【讨论】:

    【解决方案2】:

    此时的其他答案都有相关的上下文。但要知道的关键是,如果计时器回调正在向不同线程中的插槽发出信号,那么该连接要么是 QueuedConnection 要么是 BlockingQueuedConnection。

    因此,如果您使用计时器来尝试完成某种常规处理,那么这会给您在计时器触发和插槽实际执行之间的计时带来一些额外的抖动,因为接收对象在它自己的线程运行一个独立的事件循环。这意味着当事件被放入队列时它可能会执行任意数量的其他任务,并且在它完成处理这些事件之前,图片线程不会执行您的计时器事件。

    计时器应该与照片逻辑在同一个线程中。将计时器放在与相机拍摄相同的线程中,使连接直接,并为您的时间间隔提供更好的稳定性。特别是如果照片捕获和保存偶尔会出现异常持续时间。

    它是这样的,假设间隔是 10 秒:

    • 将计时器设置为 10 秒
    • 定时器触发
    • 保存开始时间
    • 拍照
    • 将照片保存到磁盘(说因为某些奇怪的原因需要 3 秒)
    • 计算 10-(当前时间 - 开始时间)= 7 秒
    • 设置超时 7 秒

    您还可以在此处设置一些逻辑来检测跳过的间隔(比如其中一项操作需要 11 秒才能完成...

    【讨论】:

    • 感谢您让我们重回正轨。 :) 无论如何,我知道您在这里提倡使用singleShot() 计时器。更新间隔的好主意,如果值为负,我还会知道照片是否超过一个间隔......需要处理这个。 :)
    • 不使用线程并使用固定 10 秒间隔的计时器的最简单解决方案有什么问题?你能详细说明一下吗?它确实考虑了连接到超时的插槽执行的时间。如果当前时间和最后假定执行之间的时间大于 10 秒,则不执行任何操作而不是拍摄(这会清空所有等待事件的事件队列(如果有))。我经常使用它以固定速率进行 OpenGL 渲染,即使渲染槽在每次执行时花费不同的时间,它的工作也非常精确(我确实测量过)。
    • @Boris 更简单的解决方案是首选,除非存在您正在执行的处理可能会超出时间间隔的风险并且您希望在坚持原始时间间隔的同时恢复处理边界。最初的发帖人暗示,在照片拍摄中可能会出现硬件卡顿等特殊情况,并且似乎对这些界限感到担忧。
    • 正如我所说,我有 90% 的把握即使在这种情况下,您也不必触摸计时器间隔。如果上次执行时间过长(例如,为 35 秒),只需检查插槽,如果是,则对接下来的 n 次执行(在这种情况下为 3 次执行)不执行任何操作,计时器将返回以多次发出超时t0+10*k,其中 t0 是开始时间。但是,当我有时间 100% 确定时,我会试一试,如果我是正确的,请将其作为答案发布。 (如果我错了,请在此处发表评论;-))
    • +1 ;-) 我做了some experiments。如果停顿时间长于计时器间隔的两倍(即错过两次或更多点击),您确实是对的。否则,如果只可能错过一次单击(这是我一直处于的情况),则计时器会正确赶上。因此,如果您对停顿可能需要多长时间没有上限(这显然是 OP 的情况),那么您的解决方案就是正确的。
    【解决方案3】:

    您可以将Qt::(Blocking)QueuedConnection 连接类型用于连接方法,以避免立即触发的直接连接。

    由于您有单独的线程,因此您应该使用阻塞版本。但是,当您希望避免在没有单独线程的接收器的情况下直接调用时,您应该考虑非阻塞变体。

    详情请见official documentation

    为了您的方便,请参阅文档:

    Qt::QueuedConnection

    当控制返回到接收者线程的事件循环时调用该槽。该槽在接收者的线程中执行。

    Qt::BlockingQueuedConnection

    与 QueuedConnection 相同,除了当前线程阻塞直到槽返回。这种连接类型应该只用于发射器和接收器在不同线程中的情况。

    你可能想要写的是你不希望有直接连接而不是排队

    QCoreApplication::removePostedEvents ( QObject * receiver, int eventType ) 事件类型为MetaCall 可以使用或清理队列,如果它已经被那些繁重的任务饱和。此外,如果已设置,您始终可以使用一个标志与插槽进行通信以退出。

    详情请参阅以下论坛讨论:http://qt-project.org/forums/viewthread/11391

    【讨论】:

      【解决方案4】:

      答案是肯定的。当您的 QTimer 和您的接收器位于不同的线程中时,调用将放入接收器事件队列中。如果您的拍照或保存程序占用了执行时间,您的活动可能会被大大延迟。但这对所有事件都是一样的。如果例程没有将控制权交还给事件循环,您的 gui 就会挂起。您可以使用:

      Qt::BlockingQueuedConnection 与 QueuedConnection 相同,除了 当前线程阻塞,直到槽返回。这种连接类型 只应在发射器和接收器不同的情况下使用 线程。

      但这种情况很可能暗示你的逻辑有问题。

      【讨论】:

      • 你知道队列有多深吗?一次可以有多少信号卡在那里?有没有办法手动清空队列?
      • @neuviemeporte: QCoreApplication::removePostedEvents ( QObject * receiver, int eventType ) ...但当然,如果设置,您总是可以使用原子标志与插槽进行通信以退出。我应该将这些信息放入我的回复中吗?
      • 不,我不知道事件队列有多深。而且,据我所知,由于没有记录,因此您无论如何都不应该使用此信息。可以随任何 Qt 更新而改变。信号/槽的事件类型是Event::MetaCall。因此,您可以按照 Laszlo Papp 的建议尝试 QCoreApplication::removePostedEvents。
      • 事件循环的最大尺寸有记录吗?在哪里?
      • 这就是我来这里的原因之一:面对我从未想过的问题。我试着检查。在一个非官方消息来源中声称队列仅受内存限制。会有意义。我不知道,为什么要人为地限制它。如果将事件对象放在堆栈上,这可能是一个限制。我想我明天必须调查 qt 来源。这有点让我感兴趣。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-12-25
      • 1970-01-01
      • 2019-06-05
      • 2020-02-27
      • 1970-01-01
      相关资源
      最近更新 更多