Qt中,工作线程在无限循环期间停止处理事件

3
我在Qt中创建了一个Worker对象,用于无限处理视频输入,然后将其移入QThread中以保持UI线程运行。问题是,我设计它的方式是视频捕获功能在无限循环中运行,直到被标志打断,而标志应该由Worker对象中的槽设置,但由于Worker对象位于无限循环内部,它从未处理此“退出”槽(或者至少我认为是这样)。我依赖于外部库,因此替换视频轮询为另一种方法并不是真正的选择。有人可以确认这确实是问题,并提出解决方案吗?以下是代码:
class worker : public QObject{
    Q_OBJECT
public:
    worker(QObject* parent = NULL);
    ~worker(){}
    Q_SLOT void process();
    Q_SLOT void stop();
private:
    bool quit;
};

worker::worker(QObject *parent) : QObject(parent){
    quit = false;
}
void worker::process(){
    while(!quit){
        //this library call puts the thread to sleep until a frame is available
        WaitForVideoFrame();
    }
}
void worker::stop(){
    quit = true;
}

然后从UI对象中,我有:

MyWorker = new worker();
QThread* thread = new QThread;
MyWorker->moveToThread(thread);
connect(thread, SIGNAL(started()), MyWorker, SLOT(process()));
QPushButton* stop_button = new QPushButton(this);
connect(stop_button, SIGNAL(clicked(bool)), MyWorker, SLOT(stop()));
thread->start();

这里的问题是当我按下stop_button时,没有任何反应,worker仍在运行循环。也许有一个函数可以从无限循环中调用以将处理时间让给事件循环?或者有更好的设计/解决方案吗?欢迎提出任何建议。


这个线程运行了多久? - jonspaceharper
它可能会运行整整一天,Jon。8、10个小时不间断。 - Armando Martins
2个回答

0

你的假设是正确的。每个 QThread 都有自己的 QEventLoop,当你在不同的线程上发射信号时,它将被排队到目标 QEventLoop 中进行处理。一旦你的工作线程的 QEventLoop 正在处理事件,任何额外发射的信号都将被排队并在当前槽函数完成后进行处理。

然而,修复这个问题应该不会太难;只需要小心一点,你可以安全地从 QThread 派生,并使长轮询工作。如果你不需要可恢复的操作,QThread 甚至有一个属性可以告诉你是否已经请求了中断 - 检查 QThread::isInterruptionRequested(),例如:

class mythread : public QThread {
    Q_OBJECT

public:
    using QThread::QThread;

    void run() override { // Main loop
        while (!isInterruptionRequested()) {
           //this library call puts the thread to sleep until a frame is available
           WaitForVideoFrame();
        }
    }
}

然而,您确实需要对正确的关闭顺序(可能在您的MainWindow析构函数中)进行小心处理:

void stopThread() {
    if (m_thread != nullptr) {
        m_thread->requestInterruption();
        m_thread->exit();
        m_thread->wait();
    }
}

编辑:上面的示例假设您不需要完全工作的信号和插槽;如果您需要,可以更新run()方法在等待时处理它:

class mythread : public QThread {
    Q_OBJECT

public:
    using QThread::QThread;

signals:
    void alive();

public slots:
    void onTimeout() { qDebug() << "timeout!"; }

    void run() override {  // Main loop
        QTimer timer;
        timer.setInterval(1500);
        QObject::connect(&timer, &QTimer::timeout, this, &mythread::alive);
        timer.start();

        while (!isInterruptionRequested()) {
            //this library call puts the thread to sleep until a frame is available
            WaitForVideoFrame();

            // Process event queue
            eventDispatcher()->processEvents(QEventLoop::AllEvents);
        }
    }
};

然后您可以按照通常的方式将其连接起来:

mythread t;
t.start();

QTimer timer;
timer.setInterval(1000);

QObject::connect(&t, &mythread::alive, [] {
    qDebug() << "I'm alive";
});

QObject::connect(&timer, &QTimer::timeout, &t, &mythread::onTimeout);

timer.start();

1
谢谢您的建议,shrpq。不过我想问一下,这是否符合指南,即不应该从QThread派生来实现处理,只能添加线程功能?如果我直接从QThread派生,会遇到什么问题吗? - Armando Martins
@ArmandoMartins 是的,你会遇到问题。上面的实现不会导致事件循环处理事件,这意味着排队的信号和线程间事件将永远不会传播。你的标志也应该是原子的或受互斥锁保护。 - jonspaceharper
你的 mythread 实现缺少事件循环,因为 run 的默认实现调用了 exec,而这是启动事件循环的地方! - jonspaceharper
@ArmandoMartins 如果您不需要Qt事件处理程序,完全可以安全有效地子类化QThread,并且从未运行事件循环。详见 https://woboq.com/blog/qthread-you-were-not-doing-so-wrong.html 例如像Qt的曼德博集示例中所使用的:https://doc.qt.io/qt-5/qtcore-threads-mandelbrot-example.html - king_nak
@king_nak他的worker对象使用插槽,这意味着事件。他需要运行一个事件循环。=/(当然,他可以进行重构,只需告诉线程退出而不是对象,但他仍然不需要将QThread子类化。) - jonspaceharper
显示剩余4条评论

0

使用事件分发器

首先,这里是关于QAbstractEventDispatcher的文档,它使得排队信号和跨线程事件传播成为可能。

thread()->eventDispatcher()->processEvents();

在循环中的某个位置添加上述代码,以便在合适的时间间隔内调用它。它会检索当前线程,然后检索线程的事件分派器,并最终强制派发程序处理任何待处理事件(其中包括传入的排队信号)。

使用线程安全的方法来通知需要停止

不要使用线程不安全的标志,而是调用所属线程的requestInterruption()函数。

随后,可以调用QThread::isInterruptionRequested()。如果返回值为true,则可以假定现在是停止正在进行的工作的时候了。

最终你的循环将类似于如下代码:

void worker::process(){
    while(!thread->isInterruptionRequested()){
        thread()->eventDispatcher()->processEvents();
        if (thread()->isInterruptionRequested())
            break;
        WaitForVideoFrame();
    }
}

在这个循环中,你有两个可能会长时间运行的调用,一个是处理事件的调用,另一个是WaitForVideoFrame()。我已经设置了循环来检查是否应该在两者之后退出。标准免责声明适用:在您的用例中测试此性能(特别是,在循环中两次检查中断请求可能过于激进)。

你应该子类化QThread吗?

相关评论:在Qt中从moveToThread和派生QThread


感谢大家的反馈和有价值的讨论。我决定采用Jon Harper的建议,因为我确实需要插槽机制(我正在使用信号发送视频帧),而且还有其他方面也需要。但是所有的建议都让我清楚地了解了不同的方法及其用途,再次感谢大家! - Armando Martins

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接