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

时间:2013-09-07 21:33:38

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

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

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

我希望在这种情况下调用被删除而不是排队,更有用的是能够在发生时作出反应(警告用户,终止执行)。

4 个答案:

答案 0 :(得分:6)

此刻的其他答案都有相关背景。但关键是要知道如果定时器回调是在另一个线程中发出信号,那么该连接就是QueuedConnection或BlockingQueuedConnection。

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

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

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

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

你也可以在这里设置一些逻辑来检测跳过的间隔(比如其中一个操作需要11秒才能完成......

答案 1 :(得分:4)

我在经过一些实验后详细说明了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 

<强>结论

如果停顿可能比定时器间隔的两倍,则必须使用的解决方案,否则不需要它,并且如上所述的简单实现效果很好。如果你更喜欢以下行为:

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。如果是真的,拍下照片,如果是假的,什么都不做。

答案 2 :(得分: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

答案 3 :(得分:3)

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

  

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

但很可能像这样的情况暗示你的逻辑出了问题。