QThread 不会退出 [英] QThread will not exit

查看:162
本文介绍了QThread 不会退出的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

当我关闭我的应用程序时,该线程仍在运行,即使它本应结束.以下代码将简单地挂在 workerThread->wait(); 行上.

When I close my application, the thread is still running even though it should have ended. The following code will simply hang on the workerThread->wait(); line.

主线程

#include "mainwindow.h"
#include "ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    workerThread = new QThread();
    worker = new Worker();

    worker->moveToThread(workerThread);

    connect(this, SIGNAL(ThreadStopSignal()), worker, SLOT(ThreadStopSlot()));
    connect(this, SIGNAL(StartWorkerSignal()), worker, SLOT(RunSlot()));
    connect(worker, SIGNAL(finished()), workerThread, SLOT(quit()));
    connect(worker, SIGNAL(finished()), worker, SLOT(deleteLater()));
    connect(workerThread, SIGNAL(finished()), workerThread, SLOT(deleteLater()));

    workerThread->start();
    emit(StartWorkerSignal());
    qDebug()<<"Main thread: " << QThread::currentThreadId();
}

MainWindow::~MainWindow()
{
    qDebug() << "Asking threads to exit" << QThread::currentThreadId();
    emit(ThreadStopSignal());

    workerThread->wait();

    qDebug() << "thread is dead";
    delete ui;
}

Worker 实现

#include "worker.h"

#include <QCoreApplication>

Worker::Worker()
{
    allowRun = true;
}

void Worker::RunSlot()
{
    qDebug() << "Starting";
    int i = 0;
    while(allowRun)
    {
        QThread::msleep(1000);
        QCoreApplication::processEvents();
        qDebug() << i++ << QThread::currentThreadId();
    }
    emit finished();
    qDebug() << "Done counting";
}

void Worker::ThreadStopSlot()
{
    allowRun = false;
    qDebug() << "Ending HID WORKER" << QThread::currentThreadId();
}

典型的运行将产生以下输出(Qt 5.1.1 clang x86_64)*:

A typical run will produce the following output (Qt 5.1.1 clang x86_64)*:

Main thread:  0x7fff743b2300 
Starting 
0 0x10b6e1000 
1 0x10b6e1000 
2 0x10b6e1000 
3 0x10b6e1000 
4 0x10b6e1000 
5 0x10b6e1000 
6 0x10b6e1000 
7 0x10b6e1000 
8 0x10b6e1000 
9 0x10b6e1000 
10 0x10b6e1000 
11 0x10b6e1000 
Asking threads to exit 0x7fff743b2300 
Ending HID WORKER 0x10b6e1000 
12 0x10b6e1000 
Done counting 

然而,应用程序仍将运行,只是没有用户界面;它偶尔会崩溃,导致 Apple 发送崩溃报告对话框打开.

However, the application will still be running, just with no UI; it will occasionally crash, causing an Apple send-crash-report dialog to open.

*是的,我目前使用的是稍旧的 Qt 版本.不过,我已经在较新的版本上对此进行了测试,并取得了类似的结果.

*Yes, I'm currently stuck with a slightly older Qt version. However, I have tested this on newer builds and have achieved similar results.

推荐答案

问题很简单:当你等待线程完成时,你阻塞了主线程中的事件循环.然而,与此同时,事件循环必须接收 workerThread->quit() 调用.这样你就陷入了僵局.

The problem is rather simple: You block the event loop in the main thread when you wait for the thread to finish. Yet, at the same time, the event loop must receive the workerThread->quit() call. Thus you have a deadlock.

简单的解决方法是在 wait() 之前显式 quit() 线程.您可以通过修复 QThread 的固有缺陷来实现这一点.请参阅下面的 Thread 类的安全实现.然后您可以简单地在 MainWindow 的析构函数中析构线程.

The simple fix is to explicitly quit() the thread before wait()ing for it. You achieve this by fixing the inherent brokenness of QThread. See below for a safe implementation of the Thread class. You can then simply destruct the thread in MainWindow's destructor.

唉,代码有一些反模式和伏都教.

Alas, the code is has some antipatterns and voodoo.

  1. workerThreadworker 都不应该是指针.这是过早的悲观,因为您添加了额外的堆分配和额外的间接层.这是毫无意义的,并迫使您在不需要的情况下进行手动内存管理.

  1. Neither workerThread nor worker should be pointers. This is premature pessimization since you add an extra heap allocation and an extra layer of indirection. It's pointless, and forces you to do manual memory management where none is called for.

threadStopSlot 是线程安全的,没有理由从工作线程的事件循环中调用它.你可以直接调用它.当您这样做时,您不需要在 runSlot 中调用 QCoreApplication::processEvents.当你这样做时,你重新进入事件循环,突然间你在该线程中运行的所有对象都受到可重入要求的约束,并且必须进行审计.坏主意 - 不要这样做.

The threadStopSlot is thread-safe and there's no reason to invoke it from the worker thread's event loop. You can call it directly. When you do so, you don't need to call QCoreApplication::processEvents in runSlot. When you do that, you re-enter the event loop, and suddenly all of your objects running in that thread are subject to reentrancy requirement and must be audited as such. Bad idea - don't do it.

由于您可能希望让事件循环在工作线程中运行,您应该反转控制:不要将控制保留在 runSlot 中,而是将其保留在事件循环中,并且让事件循环重复调用 runSlot.这就是零超时计时器习语的用途.

Since you probably wish to let the event loop run in the worker's thread, you should invert the control: instead of keeping the control in runSlot, keep it in the event loop, and have the event loop call runSlot repeatedly. That's what zero-timeout timer idiom is for.

初始化列表产生了惯用的 C++.使用它们.

Initializer lists give rise to idiomatic C++. Use them.

emit 是一个前缀,而不是一个函数.你发出fooSignal(),而不是emit(fooSignal()).这是风格问题,没错,但 emit 是用于文档目的.它仅供人类消费,如果没有额外的括号包裹,我们人类会更容易阅读.如果您不关心文档方面,请根本不要使用 emit.信号是具有机器生成实现的常规方法.您无需以任何特殊方式调用它们.

emit is meant as a prefix, not a function. You emit fooSignal(), not emit(fooSignal()). It's a matter of style, true, but emit is for documentation purposes. It is only meant for human consumption, and we humans read things easier when they are not wrapped in an extra layer of parentheses. If you don't care for the documentation aspect, don't use emit at all. Signals are regular methods with machine-generated implementation. You don't need to call them in any special way.

由于您使用 Qt 5,您应该使用编译时检查的 connect 语法.这不需要 C++11 编译器 - 除非您也使用 lambdas.

Since you use Qt 5, you should be using compile-time-checked connect syntax. This does not require a C++11 compiler - not unless you use lambdas too.

在窗口的析构函数中要求线程退出可能是一个坏主意,因为当工作线程退出时,您可能在主线程中有其他事情要做 - 需要事件循环的事情运行.

It's probably a bad idea to ask the threads to quit in the destructor of a window, since you might have other things to do in the main thread as the worker threads quit - things that necessitate the event loop to be running.

窗口被破坏的唯一方法是它是否具有 WA_DeleteOnClose 属性,或者如果您已经退出主事件循环并在退出时破坏窗口 main().您应该捕获窗口的关闭事件并设置一些动作,以便只有当所有应该完成的事情都完成时,窗口才会被删除.

The only way a window would be destructed is if it has WA_DeleteOnClose attribute, or if you've quit the main event loop and are destructing the window as you exit main(). You should catch the window's close event and set things in motion such that the window gets deleted only when everything that should be done, is done.

您可以输出线程本身,而不是将线程 id 输出到 qDebug().您可以利用线程是对象的事实,并且可以为它们提供人类可读的名称.然后,您无需手动比较线程 ID 或地址,只需读取线程名称即可.

Instead of outputting the thread id to qDebug(), you can output the thread itself. You can leverage the fact that threads are objects and you can give them human readable names. You then don't need to compare thread ids or addresses manually, you simply read the thread's name.

综上所述,如果你让我写代码,我会这样做.

Given all the above, if you asked me to write the code, I'd do it as follows.

首先,worker 功能可以抽象为一个worker base:

First, the worker functionality can be abstracted out into a worker base:

#include <QtWidgets>

class WorkerBase : public QObject {
   Q_OBJECT
   Q_PROPERTY(bool active READ isActive WRITE setActive)
   QBasicTimer m_runTimer;
   bool m_active;
protected:
   void timerEvent(QTimerEvent * ev) {
      if (ev->timerId() != m_runTimer.timerId()) return;
      work();
   }
   virtual void workStarted() {}
   virtual void work() = 0;
   virtual void workEnded() {}
public:
   WorkerBase(QObject * parent = 0) : QObject(parent), m_active(false) {
      setActive(true);
   }
   /// This method is thread-safe.
   bool isActive() const { return m_active; }
   /// This method is thread-safe.
   void setActive(bool active) {
      QObject source;
      QObject::connect(&source, &QObject::destroyed, this, [this,active]{
         // The functor is executed in the thread context of this object
         if (m_active == active) return;
         if (active) {
            m_runTimer.start(0, this);
            workStarted();
         } else {
            m_runTimer.stop();
            workEnded();
         }
         m_active = active;
      }, thread() ? Qt::QueuedConnection : Qt::DirectConnection);
   }
   ~WorkerBase() {
      Q_ASSERT(QThread::currentThread() == thread() || !thread());
      setActive(false);
   }
};

然后,工人变得直截了当:

Then, the worker becomes straightforward:

class Worker : public WorkerBase {
   Q_OBJECT
   int m_runCount;
protected:
   void workStarted() Q_DECL_OVERRIDE {
      qDebug() << "Starting" << QThread::currentThread();
   }
   void work() Q_DECL_OVERRIDE {
      QThread::msleep(1000);
      ++ m_runCount;
      qDebug() << m_runCount << QThread::currentThread();
   }
   void workEnded() Q_DECL_OVERRIDE {
      qDebug() << "Finishing" << QThread::currentThread();
      emit finished();
   }
public:
   Worker(QObject * parent = 0) : WorkerBase(parent), m_runCount(0) {}
   Q_SIGNAL void finished();
};

最后,我们添加了安全线程实现,以及一个主窗口,该窗口将控制点保留在事件循环中,并让循环运行,直到一切准备好以销毁窗口:

Finally, we add the safe thread implementation, and a main window that keeps the locus of control in the event loop, and lets the loop run until everything is ready for the window to be destructed:

class Thread : public QThread {
   using QThread::run; // final method
public:
   Thread(QObject * parent = 0) : QThread(parent) {}
   ~Thread() { quit(); wait(); }
};

class MainWindow : public QMainWindow {
   Q_OBJECT
   Worker m_worker;
   Thread m_workerThread;
   QLabel m_label;
protected:
   void closeEvent(QCloseEvent * ev) {
      if (m_worker.isActive()) {
         m_worker.setActive(false);
         ev->ignore();
      } else
         ev->accept();
   }
public:
   MainWindow(QWidget * parent = 0) : QMainWindow(parent),
      m_label("Hello :)\nClose the window to quit.")
   {
      setCentralWidget(&m_label);
      m_workerThread.setObjectName("m_worker");
      m_worker.moveToThread(&m_workerThread);
      connect(&m_worker, &Worker::finished, this, &QWidget::close);
      m_workerThread.start();
      qDebug() << "Main thread:" << QThread::currentThread();
   }
   ~MainWindow() {
      qDebug() << __FUNCTION__ << QThread::currentThread();
   }
};

int main(int argc, char ** argv)
{
   QApplication a(argc, argv);
   QThread::currentThread()->setObjectName("main");
   MainWindow w;
   w.show();
   w.setAttribute(Qt::WA_QuitOnClose);
   return a.exec();
}

#include "main.moc"

请注意,您的方法所需的大量信号/时隙明显缺失.您只需要在工作对象完成后,您就可以关闭窗口并将所有对象撕成碎片.

Note the notable absence of the plethora of signals/slots that your approach necessitated. All you care for is when the worker object has finished, you can then close the window and rip all objects to shreds.

请注意,声明顺序很重要.C++ 的语义在这方面不是随机的:顺序是有意义的.工作线程必须在工作对象之后声明,因为它们会以相反的顺序被销毁.因此,线程首先退出并销毁.此时,m_worker->thread() == nullptr 并且您可以从任何线程(包括主线程)销毁该工作线程.如果工作线程仍然存在,那将是一个错误 - 然后您需要在其自己的线程中销毁工作线程.

Note that the declaration order is important. C++'s semantics are not random in this respect: the order has meaning. The worker thread must be declared after the worker object, since they will be destructed in the reverse order. Thus, the thread is quit and destroyed first. At that point, m_worker->thread() == nullptr and you can destroy the worker from any thread, including the main thread. It'd be an error if the worker's thread had still existed - you'd then need to destroy the worker in its own thread.

输出:

Main thread: QThread(0x7fa59b501180, name = "main")
Starting QThread(0x7fff5e053af8, name = "m_worker")
1 QThread(0x7fff5e053af8, name = "m_worker")
2 QThread(0x7fff5e053af8, name = "m_worker")
3 QThread(0x7fff5e053af8, name = "m_worker")
Finishing QThread(0x7fff5e053af8, name = "m_worker")
~MainWindow QThread(0x7fa59b501180, name = "main")

这篇关于QThread 不会退出的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆