在 Qt 中使用多线程时的事件循环和信号槽处理

Event loops and signal-slot processing when using multithreading in Qt

我在使用 QThreads 时遇到了一些问题,这让我在找到合适的组合之前探索了不同的组合。然而,当涉及到事件循环和信号槽处理时,我仍然不完全理解在下面显示的四种情况下到底发生了什么。

我在 OUTPUT 部分添加了一些评论,但如您所见,我不确定我对导致观察到的行为的原因的假设是否正确。另外我不确定 case 3 是否可以在实际代码中使用。这是我的测试代码(每种情况下只有 main.cpp 不同):

worker.h:

#include <QObject>
#include <QDebug>
#include <QThread>

class Worker : public QObject
{
    Q_OBJECT
public:
    explicit Worker(QObject *parent = 0) { this->isRunning_ = false;}
    bool isRunning() const { return isRunning_; }

signals:
    void processingFinished();
    void inProgress();

public slots:
    void process()
    {
        this->isRunning_ = true;
        qDebug() << this << "processing started";
        for (int i = 0; i < 5; i++)
        {
            QThread::usleep(1000);
            emit this->inProgress();
        }
        qDebug() << this << "processing finished";
        this->isRunning_ = false;
        emit this->processingFinished();
    }

private:
    bool isRunning_;
};

workermanager.h:

#include "worker.h"

class WorkerManager : public QObject
{
    Q_OBJECT
public:
    explicit WorkerManager(QObject *parent = 0) :
        QObject(parent) {}

public slots:
    void process()
    {
        QThread *thread = new QThread();
        Worker  *worker = new Worker();

        connect(thread,SIGNAL(started()),worker,SLOT(process()));
        connect(worker,SIGNAL(processingFinished()),this,SLOT(slot1()));
        connect(worker,SIGNAL(inProgress()),this,SLOT(slot2()));
        worker->moveToThread(thread);

        qDebug() << "starting";
        thread->start();
        QThread::usleep(500);
        while(worker->isRunning()) { }
        qDebug() << "finished";
    }

    void slot1() { qDebug() << "slot1"; }
    void slot2() { qDebug() << "slot2"; }
};

main.cpp (case 1 - no separate thread for workerManager):

#include <QCoreApplication>
#include "workermanager.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    WorkerManager* workerManager = new WorkerManager;    
    workerManager->process();
    qDebug() << "end";
    return a.exec();
}

OUTPUT - both slot1 and slot2 called at a.exec() (??? - using main event loop?):

starting 
Worker(0x112db20) processing started 
Worker(0x112db20) processing finished 
finished 
end
slot2 
slot2 
slot2 
slot2 
slot2 
slot1 

main.cpp (case 2 - workerManager moved to separate thread, but thread not started):

#include <QCoreApplication>
#include "workermanager.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    WorkerManager* workerManager = new WorkerManager;
    QThread *thread = new QThread();   
    workerManager->moveToThread(thread);       
    workerManager->process();
    qDebug() << "end";
    return a.exec();
}

OUTPUT - neither slot1 nor slot2 was called - (??? event loop associated with thread receives signals but since thread was not started slots are not called?):

starting 
Worker(0x112db20) processing started 
Worker(0x112db20) processing finished 
finished 
end

main.cpp (case 3 - workerManager moved to separate thread, thread started but workerManager::process() called via workerManager->process()):

#include <QCoreApplication>
#include "workermanager.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    WorkerManager* workerManager = new WorkerManager;
    QThread *thread = new QThread();   
    workerManager->moveToThread(thread); 
    thread->start();     
    workerManager->process();
    qDebug() << "end";
    return a.exec();
}

OUTPUT - slot2 called while Worker still executing its process() (???):

starting 
Worker(0x197bb20) processing started 
slot2 
slot2 
slot2 
slot2 
Worker(0x197bb20) processing finished 
finished 
end 
slot2 
slot1 

main.cpp (case 4 - workerManager moved to separate thread, thread started but workerManager::process() called using started() signal from thread):

#include <QCoreApplication>
#include "workermanager.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    WorkerManager* workerManager = new WorkerManager;
    QThread *thread = new QThread();    
    workerManager->moveToThread(thread);
    QObject::connect(thread,SIGNAL(started()),workerManager,SLOT(process()));
    thread->start();
    qDebug() << "end";
    return a.exec();
}

OUTPUT - all events processed after reaching a.exec() (???):

end 
starting 
Worker(0x7f1d700013d0) processing started 
Worker(0x7f1d700013d0) processing finished 
finished 
slot2 
slot2 
slot2 
slot2 
slot2 
slot1 

感谢任何澄清。

您得到的所有结果都是完全正确的。我将尝试解释这是如何工作的。

事件循环是 Qt 代码中处理系统和用户事件的内部循环。当你调用 a.exec() 时,主线程的事件循环开始。另一个线程的事件循环由 QThread::run.

的默认实现启动

当 Qt 决定是时候处理一个事件时,它会执行它的事件处理程序。当事件处理程序正在工作时,Qt 没有机会处理任何其他事件(除非直接由 QApplication::processEvents() 或其他一些方法给出)。一旦事件处理程序完成,控制流 returns 到事件循环,Qt 可能会执行另一个处理程序来处理另一个事件。

信号和槽与 Qt 术语中的事件和事件处理程序不同。但是槽是由事件循环处理的,有点类似。如果您的代码中有控制流(例如在 main 函数中),您可以像任何其他 C++ 函数一样立即执行任何插槽。但是当 Qt 这样做时,它只能从事件循环中做到这一点。应该注意的是,信号总是立即发送,而插槽执行可能会延迟。

现在让我们看看每种情况下会发生什么。

案例一

WorkerManager::process 在程序开始时直接执行。新线程启动,Worker::process立即在新线程中执行。 WorkerManager::process 继续执行直到 Worker 完成,冻结主线程中的所有其他操作(包括槽处理)。 WorkerManager::process 完成后,控制流转到 QApplication::exec。 Qt 建立与另一个线程的连接,接收有关槽调用的消息并随后调用所有这些消息。

案例二

Qt 默认在对象所属的线程中执行对象的槽。主线程不会执行 WorkerManager 的槽,因为它属于另一个线程。然而,这个线程从未启动过。它的事件循环永远不会结束。 slot1slot2 的调用将永远留在 Qt 的队列中,等待您启动线程。悲伤的故事。

案例三

在这种情况下 WorkerManager::process 在主线程中执行,因为您直接从主线程调用它。同时,WorkerManager 的线程启动。它的事件循环启动并等待事件。 WorkerManager::process 启动 Worker 的线程并在其中执行 Worker::execWorker 开始向 WorkerManager 发送信号。 WorkerManager 的线程几乎立即开始执行适当的插槽。此时 WorkerManager::slot2WorkerManager::process 同时执行似乎很尴尬。但它非常好,至少如果 WorkerManager 是线程安全的。 Worker 完成后不久,WorkerManager::process 完成并执行 a.exec() 但没有太多要处理的。

案例 4

Main 函数刚刚启动 WorkerManager 的线程并立即转到 a.exec(),导致 end 作为输出的第一行。 a.exec() 处理一些东西并确保程序执行但不执行 WorkerManager 的槽,因为它属于另一个线程。 WorkerManager::processWorkerManager 的线程中从其事件循环中执行。 Worker 的线程已启动,Worker::process 开始将信号从 Worker 的线程发送到 WorkerManager 的线程。不幸的是,后者正忙于执行 WorkerManager::process。当 Worker 完成时,WorkerManager::process 也完成并且 WorkerManager 的线程立即执行所有排队的插槽。

您的代码中最大的问题是 usleep 和无限循环。在使用 Qt 时,您几乎不应该使用它们。我知道 Worker::process 中的睡眠只是一些实际计算的占位符。但是你应该从 WorkerManager 中删除睡眠和无限循环。使用 WorkerManager::slot1 检测 Worker 的终止。如果您开发 GUI 应用程序,则无需将 WorkerManager 移动到另一个线程。它的所有方法(没有睡眠)都将快速执行并且不会冻结 GUI。