当接收器繁忙时,Qt信号会发生什么

What happens with Qt signals when the receiver is busy?

本文关键字:信号 什么 Qt 接收器      更新时间:2023-10-16

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

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

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

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

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

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

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

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

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

经过一些实验后,我在这里详细介绍了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 < expectedTime + epsilon。如果是真的,就拍照片,如果是假的,什么都不做。

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

Qt::BlockingQueuedConnection 与 QueuedConnection 相同,除了 当前线程阻塞,直到插槽返回。此连接类型 应仅在发射器和接收器位于不同位置时使用 线程。

但最有可能的是,这样的情况暗示你的逻辑有问题。

您可以对

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

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

有关详细信息,请参阅官方文档。

为方便起见,请从文档中获取:

Qt::QueuedConnection

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

Qt::阻塞排队连接

与 QueuedConnection 相同,只是当前线程阻塞,直到插槽返回。此连接类型只应在发射器和接收器位于不同线程中时使用。

您可能想写的是,您不希望直接连接而不是排队

可以使用事件类型的QCoreApplication::removePostedEvents ( QObject * receiver, int eventType ) MetaCall,或者在队列被这些繁重的任务饱和时清理队列。此外,如果设置了,您始终可以使用标志将其与插槽进行通信以退出。

有关详细信息,请参阅以下论坛讨论:http://qt-project.org/forums/viewthread/11391