11

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

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

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

4

4 回答 4

6

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

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

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

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

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

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

于 2013-09-07T21:57:30.997 回答
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总是以 1000 毫秒的倍数开始:

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. 如果是真的,就拍照,如果是假的,什么也不做。

于 2013-09-09T06:13:37.983 回答
3

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

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

详情请查看官方文档

为了您的方便,从文档中:

Qt::QueuedConnection

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

Qt::BlockingQueuedConnection

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

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

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

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

于 2013-09-07T21:38:19.270 回答
3

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

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

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

于 2013-09-07T21:42:10.853 回答