Qt 阻塞事件循环

Posted

技术标签:

【中文标题】Qt 阻塞事件循环【英文标题】:Qt blocked event loop 【发布时间】:2016-11-01 22:38:40 【问题描述】:

我最近开始使用 QT 框架。昨天我开始编写一个简单的多线程应用程序。目前我有点卡在以下问题上。

考虑两个工作类,它们都使用线程来执行一些“繁重的计算”。第一个类 FooWorker 如下所示:

class FooWorker : public QObject

    Q_OBJECT

public:
    FooWorker() : QObject()  
    ~FooWorker()  

signals:
    void notify(int);
    void aborted();

public slots:
    void doWork()
    
        int counter = 0;
        forever 

            // For the sake of this example this reassembles a heavy computational process
            if(counter++ < 10) 
                emit notify(counter);
                QThread::sleep(1);
             else 
                counter = 0;

                // Wait until we get a signal to restart the process
                mutex_.lock();
                condition_.wait(&mutex_);
                mutex_.unlock();
            
            // We should check for a cancellation flag every iteration...
        

        emit aborted();
    

private:
    QMutex mutex_;
    QWaitCondition condition_;
;

槽“doWork”将被安排在另一个线程中运行。该插槽将永远运行,并且每秒发出一个信号,直到发出 10 个通知。之后我们等到它再次被唤醒。

第二个类,BarWorker,如下所示:

class BarWorker : public QObject

    Q_OBJECT

public:
    BarWorker() : QObject()  
    ~BarWorker()  

signals:
    void aborted();

public slots:
    void doWork()
    
        forever 
            // Another heavy computational process
            QThread::sleep(1);

            // We should check for a cancellation flag every iteration...
        

        emit aborted();
    

    void onNotify(int value)
    
        qDebug() << "Notification value:" << value;
    
;

同样,插槽“doWork”将被安排在另一个线程中运行。插槽将永远运行以执行繁重的计算过程。再次,一旦该过程完成,我们将等到它再次被唤醒(为了这个例子,我在这个类中省略了它)。

最后主要看起来如下:

int main(int argc, char *argv[])

    QCoreApplication a(argc, argv);

    QThread* barThread = new QThread();
    BarWorker* barWorker = new BarWorker();
    barWorker->moveToThread(barThread);

    QThread* fooThread = new QThread();
    FooWorker* fooWorker = new FooWorker();
    fooWorker->moveToThread(fooThread);

    // Automatically deletes worker and thread
    QObject::connect(fooThread, SIGNAL(started()), fooWorker, SLOT(doWork()));
    QObject::connect(fooWorker, SIGNAL(aborted()), fooThread, SLOT(quit()));
    QObject::connect(fooWorker, SIGNAL(aborted()), fooWorker, SLOT(deleteLater()));
    QObject::connect(fooThread, SIGNAL(finished()), fooThread, SLOT(deleteLater()));

    QObject::connect(barThread, SIGNAL(started()), barWorker, SLOT(doWork()));
    QObject::connect(barWorker, SIGNAL(aborted()), barThread, SLOT(quit()));
    QObject::connect(barWorker, SIGNAL(aborted()), barWorker, SLOT(deleteLater()));
    QObject::connect(barThread, SIGNAL(finished()), barThread, SLOT(deleteLater()));

    QObject::connect(fooWorker, SIGNAL(notify(int)), barWorker, SLOT(onNotify(int)), Qt::QueuedConnection);

    fooThread->start();
    barThread->start();

    return a.exec();

当我运行应用程序时,什么都没有打印出来。这是意料之中的,因为 BarWorker 实例的事件循环被阻塞了。随着“通知”信号的发出,“onNotify”插槽被排入事件队列。因为我们在 'doWork' 槽中有一个永无止境的循环(直到我们手动中止它),所以不会调用 'onNotify' 槽。为了解决这个问题,我可以做几件事,即:

    使用 Qt::DirectConnection 标志将 'notify' 信号连接到 'onNotify' 插槽。这样,它看起来就像在信号线程上执行的正常函数调用。 偶尔调用QCoreApplication::processEvents()方法强制处理事件队列。 未知解决方案,我目前不知道 :)???

我希望有人对此问题有一些替代解决方案,甚至提出完全不同的方法,因为恕我直言,上述解决方案有些丑陋,感觉不对。

【问题讨论】:

什么是“永远”? 用于Qt中的无限循环doc.qt.io/qt-5/qtglobal.html#forever @PAVANCHANDAKA 谢谢! 【参考方案1】:

我认为这里找不到任何“神奇”的解决方案;如果线程正在运行您自己的自定义事件循环,则该线程不能运行 Qt 的事件循环。在实践中,有两种常见的解决方案,实际上是同一枚硬币的两个方面:

    按照您在问题中的建议,定期从事件循环中调用 processEvents(),以便 Qt 事件处理代码偶尔运行并处理传入的异步信号。

    在 doWork() 方法中不要有长时间运行的循环。相反,做少量工作,将该工作的结果/状态存储在成员变量或某处,然后调用类似 QTimer::singleShot(0, this, SLOT(doWork())) 的东西,以便 Qt 事件在第一次调用 doWork() 返回后不久,循环将再次调用您的 doWork() 方法。这样,Qt 事件循环的延迟时间永远不会超过单个 doWork() 调用所占用的(短暂)时间。

在这两个选项中,我认为第二个更可取,因为它允许 Qt 事件循环以正常方式运行,并且还避免了潜在的绊倒你自己的鞋带问题——例如想象一下,如果在使用解决方案 (1) 时,您对 processEvents() 的调用会导致调用删除 BarWorker 对象的槽。当 processEvents() 调用返回时,BarWorker::doWork() 将继续执行,但此时,它可能在正常执行过程中访问的所有本地成员变量和虚拟方法都已被销毁,并且可以读取或写入它们将导致未定义的行为(如果幸运的话,一个易于调试的崩溃)。使用解决方案 (2) 时不会发生这种可能的混乱,因为如果 BarWorker 对象在调用 doWork() 之间被删除,则任何排队的对 doWork() 的异步调用都将被安全地取消。

【讨论】:

【参考方案2】:

与事件循环互操作的forever 循环的习惯用法是零持续时间计时器。我们可以将其分解为WorkerBase 类,其中的工作单元将在workUnit 方法中完成:

// https://github.com/KubaO/***n/tree/master/questions/worker-timer-40369716
#include <QtCore>

// See http://***.com/q/40382820/1329652
template <typename Fun> void safe(QObject * obj, Fun && fun) 
    Q_ASSERT(obj->thread() || qApp && qApp->thread() == QThread::currentThread());
    if (Q_LIKELY(obj->thread() == QThread::currentThread()))
        return fun();
    struct Event : public QEvent 
      using F = typename std::decay<Fun>::type;
      F fun;
      Event(F && fun) : QEvent(QEvent::None), fun(std::move(fun)) 
      Event(const F & fun) : QEvent(QEvent::None), fun(fun) 
      ~Event()  fun(); 
    ;
    QCoreApplication::postEvent(
          obj->thread() ? obj : qApp, new Event(std::forward<Fun>(fun)));


class WorkerBase : public QObject 
    Q_OBJECT
    QBasicTimer timer_;
protected:
    virtual void workUnit() = 0;
    void timerEvent(QTimerEvent *event) override 
        if (event->timerId() == timer_.timerId() && timer_.isActive())
            workUnit();
    
public:
    using QObject::QObject;
    Q_SIGNAL void finished();
    /// Thread-safe
    Q_SLOT void virtual start() 
        safe(this, [=]
           timer_.start(0, this);
        );
    
    /// Thread-safe
    Q_SLOT void virtual stop() 
        safe(this, [=]
            if (!isActive()) return;
            timer_.stop();
            emit finished();
        );
    
    bool isActive() const  return timer_.isActive(); 
    ~WorkerBase() 
        if (isActive()) emit finished();
    
;

工人然后变成:

class FooWorker : public WorkerBase

    Q_OBJECT
    int counter = 0;
    bool isDone() const  return counter >= 10; 
    void workUnit() override 
        if (!isDone()) 
            counter ++;
            emit notify(counter);
            QThread::sleep(1);
         else
            stop();
    
public:
    void start() override 
        counter = 0;
        WorkerBase::start();
    
    void stop() override 
        if (!isDone()) emit aborted();
        WorkerBase::stop();
    
    Q_SIGNAL void notify(int);
    Q_SIGNAL void aborted();
;

class BarWorker : public WorkerBase

    Q_OBJECT
    void workUnit() override 
        QThread::sleep(1);
    
public:
    void stop() override 
        emit aborted();
        WorkerBase::stop();
    
    Q_SIGNAL void aborted();
    Q_SLOT void onNotify(int value)
    
        qDebug() << "Notification value:" << value;
    
;

请注意,aborted()finished() 信号具有不同的含义。

最后是测试工具:

class Thread : public QThread  public: ~Thread()  quit(); wait();  ;

int main(int argc, char ** argv) 
    QCoreApplication appargc, argv;

    BarWorker barWorker;
    FooWorker fooWorker;
    Thread barThread, fooThread;
    barWorker.moveToThread(&barThread);
    fooWorker.moveToThread(&fooThread);
    barWorker.start();
    fooWorker.start();

    QObject::connect(&fooWorker, &FooWorker::finished, &app, &QCoreApplication::quit);
    QObject::connect(&fooWorker, &FooWorker::notify, &barWorker, &BarWorker::onNotify);

    fooThread.start();
    barThread.start();
    return app.exec();


#include "main.moc"

如果您收到 QBasicTimer::stop: Failed. Possibly trying to stop from a different thread 警告,则没有任何影响,并且是 Qt 错误的结果。

【讨论】:

以上是关于Qt 阻塞事件循环的主要内容,如果未能解决你的问题,请参考以下文章

在插槽中调用 QDialog::exec 会阻塞主事件循环吗?

QT事件循环

QT中的线程与事件循环理解

Qt ------ 主事件循环与 QEventLoop

将ZMQ事件循环与QT / Pyforms事件循环相结合

Qt源码阅读 事件循环