Loading

【QT】 Qt多线程的“那些事”

作者:李春港
出处:https://www.cnblogs.com/lcgbk/p/13967448.html

一、前言

在我们开发Qt程序时,会经常用到多线程和信号槽的机制,将耗时的事务放到单独的线程,将其与GUI线程独立开,然后通过信号槽的机制来进行数据通信,避免GUI界面假死的情况。例如:使用QT实现文件的传送,并且GUI界面需要实时显示发送的进度,这时就需要将耗时的文件数据操作放到独立的线程中,然后把已传送的进度数据通过信号发送到GUI线程,GUI主线程接收到信号后通过槽函数来更新UI,这样界面就不会出现假死的情况了。
多线程信号槽机制都是QT的关键技术之一。理解清楚这两个技术点的关系,会让你在开发过程中少走些弯路,少踩一些坑。本文章会介绍多种Qt多线程的实现方法,但是主要还是介绍有关于 信号槽机制的多线程 实现方法。在学习QT多线程的"那些事"前,我们不妨先思考下以下的一些问题,然后再带着问题继续往下看,这样可能会有更好的理解:
【1】如何正确使用QT的多线程?
【2】线程start后,哪里才是线程正在启动的地方?
【3】如何正确结束子线程以及资源释放?
【4】重复调用QThread::start、QThread::quit()或QThread::exit()、QThread::terminate函数会有什么影响?
【5】调用QThread::quit()或QThread::exit()、QThread::terminate函数会不会立刻停止线程?
【6】多线程之间是怎么进行通信的?
【7】如何在子线程中启动信号与槽的机制?
【8】QT中多线程之间的信号和槽是如何发送或执行的?
【9】如何正确使用信号与槽机制?

接下来我会通过我以前踩过的坑和开发经验,并且通过一些实例来总结一下QT多线程QT信号槽机制的知识点。

这个是本文章实例的源码地址:https://gitee.com/CogenCG/QThreadExample.git

二、QThread源码浅析

本章会挑出QThread源码中部分重点代码来说明QThread启动到结束的过程是怎么调度的。其次因为到了Qt4.4版本,Qt的多线程就有所变化,所以本章会以Qt4.0.1和Qt5.6.2版本的源码来进行浅析。

2.1 QThread类的定义源码

Qt4.0.1版本源码:

#ifndef QT_NO_THREAD
class Q_CORE_EXPORT QThread : public QObject
{
public:
    ...//省略
    explicit QThread(QObject *parent = 0);
    ~QThread();
    ...//省略
    void exit(int retcode = 0);

public slots:
    void start(QThread::Priority = InheritPriority); //启动线程函数
    void terminate(); //强制退出线程函数
    void quit(); //线程退出函数
    ...//省略
signals:
    void started(); //线程启动信号
    void finished(); //线程结束信号
    ...//省略
    
protected:
    virtual void run() = 0;
    int exec();
    ...//省略
};
#else // QT_NO_THREAD

Qt5.6.2版本源码:

#ifndef QT_NO_THREAD
class Q_CORE_EXPORT QThread : public QObject
{
    Q_OBJECT
public:
    ...//省略
    explicit QThread(QObject *parent = Q_NULLPTR);
    ~QThread();
    ...//省略
    void exit(int retcode = 0); //线程退出函数
    ...//省略
public Q_SLOTS:
    void start(Priority = InheritPriority); //启动线程函数
    void terminate(); //强制退出线程函数
    void quit(); //线程退出函数
    ...//省略
Q_SIGNALS:
    void started(QPrivateSignal); //线程启动信号
    void finished(QPrivateSignal); //线程结束信号
    
protected:
    virtual void run();
    int exec();
    ...//省略
};
#else // QT_NO_THREAD

从以上两个版本的代码可以看出,这些函数在声明上基本没什么差异,但是仔细看,两个版本的 run() 函数声明的是不是不一样?

  • Qt4.0.1版本run() 函数是纯虚函数,即此类为抽象类不可以创建实例,只可以创建指向该类的指针,也就是说如果你需要使用QThread来实现多线程,就必须实现QThread的派生类并且实现 run() 函数;
  • Qt5.6.2版本的run() 函数是虚函数,继承QThread类时,可以重新实现 run() 函数,也可以不实现。

注:我查看了多个Qt版本的源码,发现出现以上差异的版本是从Qt4.4开始的。从Qt4.4版本开始,QThread类就不再是抽象类了。

2.2 QThread::start()源码

再来看看QThread::start()源码,Qt4.0.1版本和Qt5.6.2版本此部分的源码大同小异,所以以Qt5.6.2版本的源码为主,如下:

void QThread::start(Priority priority)
{
    Q_D(QThread);
    QMutexLocker locker(&d->mutex);
 
    if (d->isInFinish) {
        locker.unlock();
        wait();
        locker.relock();
    }
 
    if (d->running)
        return;
        
    ... ... // 此部分是d指针配置
 
#ifndef Q_OS_WINRT

    ... ... // 此部分为注释
    
    d->handle = (Qt::HANDLE) _beginthreadex(NULL, d->stackSize, QThreadPrivate::start,
                                            this, CREATE_SUSPENDED, &(d->id));
#else // !Q_OS_WINRT
    d->handle = (Qt::HANDLE) CreateThread(NULL, d->stackSize, (LPTHREAD_START_ROUTINE)QThreadPrivate::start,
                                            this, CREATE_SUSPENDED, reinterpret_cast<LPDWORD>(&d->id));
#endif // Q_OS_WINRT
 
    if (!d->handle) {
        qErrnoWarning(errno, "QThread::start: Failed to create thread");
        d->running = false;
        d->finished = true;
        return;
    }
 
    int prio;
    d->priority = priority;
    switch (d->priority) {
    
    ... ... // 此部分为线程优先级配置
    
    case InheritPriority:
    default:
        prio = GetThreadPriority(GetCurrentThread());
        break;
    }
 
    if (!SetThreadPriority(d->handle, prio)) {
        qErrnoWarning("QThread::start: Failed to set thread priority");
    }
 
    if (ResumeThread(d->handle) == (DWORD) -1) {
        qErrnoWarning("QThread::start: Failed to resume new thread");
    }
}

挑出里面的重点来说明:

(1)Q_D()宏定义

在看源码的时候,当时比较好奇start函数的第一条语句 Q_D()宏定义 是什么意思,所以就看了下源码,在此也顺便讲讲,Q_D() 源码是一个宏定义,如下:

#define Q_D(Class) Class##Private * const d = d_func()

此处利用了预处理宏里的 ## 操作符:连接前后两个符号,变成一个新的符号。将Q_D(QThread)展开后,变成:QThreadPrivate * const d = d_func()。

(2)_beginthreadex()函数
上面d->handle = (Qt::HANDLE) _beginthreadex ( NULL, d->stackSize, QThreadPrivate::start, this, CREATE_SUSPENDED, &( d->id ) ) 语句中的函数是创建线程的函数,其原型以及各参数的说明如下:

unsigned long _beginthreadex( 
 
void *security,       // 安全属性,NULL为默认安全属性
 
unsigned stack_size,  // 指定线程堆栈的大小。如果为0,则线程堆栈大小和创建它的线程的相同。一般用0
 
unsigned ( __stdcall *start_address )( void * ), 
                      // 指定线程函数的地址,也就是线程调用执行的函数地址(用函数名称即可,函数名称就表示地址)
 
void *arglist,        // 传递给线程的参数的指针,可以通过传入对象的指针,在线程函数中再转化为对应类的指针
                        //如果传入this,这个this表示调用QThread::start的对象地址,也就是QThread或者其派生类对象本身
 
unsigned initflag,    // 线程初始状态,0:立即运行;CREATE_SUSPEND:suspended(悬挂)
 
unsigned *thrdaddr    // 用于记录线程ID的地址
 
);

2.3 QThreadPrivate::start()源码

从QThread::start()源码可以知道,QThreadPrivate::start是重点,其实际就是调用了QThreadPrivate::start(this),这个 this 表示调用QThread::start的对象地址,也就是QThread或者其派生类对象本身。因为两个Qt版本此部分的源码大同小异,所以本部分主要是以5.6.2版本的源码为主,其源码以及说明如下:

// 参数arg就是上面所说的this
unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(void *arg)
{
    QThread *thr = reinterpret_cast<QThread *>(arg);
    QThreadData *data = QThreadData::get2(thr);
 
    // 创建线程局部存储变量,存放线程id
    qt_create_tls();
    TlsSetValue(qt_current_thread_data_tls_index, data);
    data->threadId = reinterpret_cast<Qt::HANDLE>(quintptr(GetCurrentThreadId()));
 
    QThread::setTerminationEnabled(false);
 
    {
        QMutexLocker locker(&thr->d_func()->mutex);
        data->quitNow = thr->d_func()->exited;
    }
 
    if (data->eventDispatcher.load()) // custom event dispatcher set?
        data->eventDispatcher.load()->startingUp();
    else
        createEventDispatcher(data);
        
    ...//省略
    
    emit thr->started(QThread::QPrivateSignal()); // 发射线程启动信号
    QThread::setTerminationEnabled(true);
    thr->run(); // 调用QThread::run()函数 -- 线程函数
 
    finish(arg); //结束线程
    return 0;
}

由上述源码可以看出,实际上 run() 函数是在这里调用的,并且发出了 started() 启动信号,等到 run() 函数执行完毕,最后是调用了 QThreadPrivate::finish 函数结束线程,并且在finish内会发出 QThread::finished() 线程已结束的信号。

2.4 QThread::run()源码

再看看QThread::run()函数的源码。在上面 《2.1 QThread类的定义源码》的小节,我们可以看到两个Qt版本声明此方法的方式不一样,Qt-4.0版本将此定义为了纯虚函数,而Qt-5.6版本将此定义为了虚函数,那我们就看看Qt-5.6版本中,QThread::run()是如何定义的,如下:

void QThread::run()
{
    (void) exec();
}
  1. 每一个 Qt 应用程序至少有一个 事件循环 ,就是调用了 QCoreApplication::exec() 的那个事件循环。不过,QThread也可以开启事件循环。只不过这是一个受限于线程内部的事件循环。因此我们将处于调用main()函数的那个线程,并且由 QCoreApplication::exec() 创建开启的那个事件循环成为 主事件循环 ,或者直接叫 主循环 。注意,QCoreApplication::exec()只能在调用main()函数的线程调用。主循环所在的线程就是主线程,也被成为 GUI 线程,因为所有有关 GUI 的操作都必须在这个线程进行。QThread的局部事件循环则可以通过在 QThread::run() 中调用 QThread::exec() 开启。

  2. 我们通过以上源码可以看到,它的定义很简单,就是调用了一个函数:QThread::exec() 开启线程中的 事件循环 ,我们也可以通过继承QThread,重写run()函数的方式,让其实现相对复杂的逻辑代码。如果你的线程需要将某些槽函数在本线程完成的话,就必须开启事件循环,否则在线程内无法响应各种信号并作出相应的行为。

小结: 比Qt-4.4版本更早的版本中,我们使用QThread启动线程时,就必须要实现继承于QThread的派生类,并且一定要重写run函数,若需要使用事件循环,就需要在run函数中添加exec()。到了Qt4.4版本之后(包括Qt4.4版本),QThread就不是抽象类了,不派生也可以实例化,在不重写QThread::run()方法,start启动线程是默认启动事件循环的。

注:当程序跑到了exec()代码时,位于exec()后面的代码就不会再被执行,除非我们使用quit、exit等退出语句来退出事件循环,退出后,程序才会继续执行位于exec()后面的代码。

2.5 QThread::quit()、QThread::exit()、QThread::terminate()源码

线程停止函数的区别,从Qt源码来分析:

(1)QThread::quit()、QThread::exit()

//QThread::quit()声明
void quit();
//QThread::quit()定义
void QThread::quit()
{ exit(); }

//QThread::exit()声明
void exit(int retcode = 0);
//QThread::exit()定义
void QThread::exit(int returnCode)
{
    Q_D(QThread);
    QMutexLocker locker(&d->mutex);
    d->exited = true;
    d->returnCode = returnCode;
    d->data->quitNow = true;
    for (int i = 0; i < d->data->eventLoops.size(); ++i) {
        QEventLoop *eventLoop = d->data->eventLoops.at(i);
        eventLoop->exit(returnCode);
    }
}

由以上源码可知,QThread::quit()QThread::exit(0) 的调用是等效的,都是告诉线程的事件循环,以返回码0(成功)退出。如果线程没有事件,则此函数不执行任何操作,也就是无效的。当线程拥有事件循环并且正处于 事件循环(QThread::exec()) 的状态时,调用 QThread::quit()或者QThread::exit() 线程就会马上停止,否则不会立刻停止线程,直到线程处于事件循环也就是正在执行 QThread::exec() 时,才会停止线程。

如果重复调用 QThread::quit()或者QThread::exit() 会有什么影响吗?
重复调用 QThread::quit()或者QThread::exit() 也不会有什么影响,因为只有拥有事件循环的线程,这两个函数才会生效停止线程的功能。

(2)QThread::terminate()

void QThread::terminate()
{
    Q_D(QThread);
    QMutexLocker locker(&d->mutex);
    if (!d->running)
        return;
    if (!d->terminationEnabled) {
        d->terminatePending = true;
        return;
    }

// Calling ExitThread() in setTerminationEnabled is all we can do on WinRT
#ifndef Q_OS_WINRT
    TerminateThread(d->handle, 0);
#endif
    QThreadPrivate::finish(this, false); //结束线程函数
}

在这个函数定义的最后一个语句,是调用了 QThreadPrivate::finish(this, false); 函数,其函数作用是直接退出线程,无论线程是否开启了事件循环都会生效,会马上终止一个线程,但这个函数存在非常不安定因素,不推荐使用

如果重复调用 QThread::terminate() 会有什么影响吗?
没有影响。我们可以看到函数体里面的第三条语句,它首先会判断线程是否还在运行中,如果不是,会直接退出函数,就不会继续往下执行调用QThreadPrivate::finish(this, false); 函数了。

2.6 章节小结

相信看了以上的一些QThread源码,都大概知道了QThread类的本质以及QThread开启到结束的过程。这里我再简单总结下:

(1)QThread的本质:

  • QThread 是用来管理线程的,它所依附的线程和它管理的线程并不是同一个东西;
  • QThread 所依附的线程,就是执行 QThread t 或 QThread * t=new QThread 所在的线程;
  • QThread 管理的线程,就是 run 启动的线程,也就是次线程。

(2)在这里针对Qt4.4版本之后(包括Qt4.4版本)简单汇总一下线程启动到结束的过程:

  • QThread对象或者QThread派生类对象显式调用QThread类中的外部start()方法;
  • QThread::start()方法再调用QThreadPrivate::start()方法;
  • 在QThreadPrivate::start()方法内调用了QThread::run()虚函数,对使用者来说到了这里才是真正进入了一个新的线程里面。也就是说定义QThread对象或者QThread派生类对象的时候,还是在原来的线程里面,只有进入run函数才是进入了新的线程;
  • 在QThreadPrivate::start()方法调用QThread::run()虚函数结束后,就会继续调用QThreadPrivate::finish()函数来结束线程,并发出线程结束的信号finished()。

(3)QThread::quit()、QThread::exit()、QThread::terminate():

  • 对线程重复使用这三个停止线程的函数,没有任何影响;
  • 尽量不要使用QThread::terminate()停止线程,此方式是强制退出线程,没有安全保障。
  • 调用QThread::quit()和QThread::exit()一样。

(4)Qt各版本QThread类的变化:

  • Qt4.4版本之前QThread类是属于抽象类, Qt4.4版本之后(包括4.4版本)不是抽象类。

三、四种Qt多线程的实现方法

Qt的多线程实现方法主要有四种形式:子类化QThread、子类化QObject+moveToThread、继承QRunnable+QThreadPool、QtConcurrent::run()+QThreadPool。本文章会注重介绍前两种实现方法:子类化QThread、子类化QObject+moveToThread,也会简单介绍后两种的使用。
注:QtConcurrent、QRunnable以及QThreadPool的类,在Qt-4.4版本才开始有。

3.1 子类化QThread

子类化QThread来实现多线程, QThread只有run函数是在新线程里的,其他所有函数都在QThread生成的线程里。正确启动线程的方法是调用QThread::start()来启动,如果直接调用run成员函数,这个时候并不会有新的线程产生( 原因: 可以查看第一章,run函数是怎么被调用的)

3.1.1 步骤

  • 子类化 QThread;
  • 重写run,将耗时的事件放到此函数执行;
  • 根据是否需要事件循环,若需要就在run函数中调用 QThread::exec() ,开启线程的事件循环。事件循环的作用可以跳到《2.4 QThread::run()源码》小节进行阅读;
  • 为子类定义信号和槽,由于槽函数并不会在新开的线程运行,所以需要在构造函数中调用 moveToThread(this)。 注意:虽然调用moveToThread(this)可以改变对象的线程依附性关系,但是QThread的大多数成员方法是线程的控制接口,QThread类的设计本意是将线程的控制接口供给旧线程(创建QThread对象的线程)使用。所以不要使用moveToThread()将该接口移动到新创建的线程中,调用moveToThread(this)被视为不好的实现。

接下来会通过《使用线程来实现计时器,并实时在UI上显示》的实例来说明不使用事件循环和使用事件循环的情况。(此实例使用QTimer会更方便,此处为了说明QThread的使用,故使用线程来实现)

3.1.2 不使用事件循环实例

InheritQThread.hpp

class InheritQThread:public QThread
{
    Q_OBJECT
public:
    InheritQThread(QObject *parent = Q_NULLPTR):QThread(parent){
        
    }
    
    void StopThread(){
        QMutexLocker lock(&m_lock);
        m_flag = false;
    }
    
protected:
    //线程执行函数
    void run(){
        qDebug()<<"child thread = "<<QThread::currentThreadId();
        int i=0;
        m_flag = true;
        
        while(1)
        {
            ++i;
            emit ValueChanged(i); //发送信号不需要事件循环机制
            QThread::sleep(1);
            
            {
                QMutexLocker lock(&m_lock);
                if( !m_flag )
                    break;
            }
            
        }
    }
    
signals:
    void ValueChanged(int i);
    
public:
    bool m_flag;
    QMutex m_lock;
};

mainwindow.hpp

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = nullptr) :
        QMainWindow(parent),
        ui(new Ui::MainWindow){
        ui->setupUi(this);
        
        qDebug()<<"GUI thread = "<<QThread::currentThreadId();
        WorkerTh = new InheritQThread(this);
        connect(WorkerTh, &InheritQThread::ValueChanged, this, &MainWindow::setValue);
    }
    
    ~MainWindow(){
        delete ui;
    }
    
public slots:
    void setValue(int i){
        ui->lcdNumber->display(i);
    }
    
private slots:
    void on_startBt_clicked(){
        WorkerTh->start();
    }
    
    void on_stopBt_clicked(){
        WorkerTh->StopThread();
    }
    
    void on_checkBt_clicked(){
        if(WorkerTh->isRunning()){
            ui->label->setText("Running");
        }else{
            ui->label->setText("Finished");
        }
    }
    
private:
    Ui::MainWindow *ui;
    InheritQThread *WorkerTh;
};

在使用多线程的时候,如果出现共享资源使用,需要注意资源抢夺的问题,例如上述InheritQThread类中m_flag变量就是一个多线程同时使用的资源,上面例子使用 QMutexLocker+QMutex 的方式对临界资源进行安全保护使用,其实际是使用了 RAII技术:(Resource Acquisition Is Initialization),也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源。具体 QMutexLocker+QMutex 互斥锁的原理以及使用方法,在这里就不展开说了,这个知识点网上有很多非常好的文章。

效果:

(1)在不点【start】按键的时候,点击【check thread state】按钮检查线程状态,该线程是未开启的。

(2)按下【start】后效果如下,并查看终端消息打印信息:

只有调用了QThread::start()后,子线程才是真正的启动,并且只有在run()函数才处于子线程内。

(3)我们再试一下点击【stop】按钮,然后检查线程的状态:

点击【stop】按钮使 m_flag = false, 此时run函数也就可以跳出死循环,并且停止了线程的运作,之后我们就不能再次使用该线程了,也许有的人说,我再一次start不就好了吗?再一次start已经不是你刚才使用的线程了,这是start的是一个全新的线程。到此子类化 QThread ,不使用事件循环的线程使用就实现了,就这么简单。

3.1.3 使用事件循环实例

run函数中的 while 或者 for 循环执行完之后,如果还想让线程保持运作,后期继续使用,那应该怎么做?
可以启动子线程的事件循环,并且使用信号槽的方式继续使用子线程。注意:一定要使用信号槽的方式,否则函数依旧是在创建QThread对象的线程执行。

  • 在run函数中添加QThread::exec()来启动事件循环。(注意: 在没退出事件循环时,QThread::exec()后面的语句都无法被执行,退出后程序会继续执行其后面的语句);
  • 为QThread子类定义信号和槽;
  • 在QThread子类构造函数中调用 moveToThread(this)(注意: 可以实现构造函数在子线程内执行,但此方法不推荐,更好的方法会在后面提到)。

接着上述的实例,在InheritQThread类构造函数中添加并且调用moveToThread(this);在run函数中添加exec();并定义槽函数:

/**************在InheritQThread构造函数添加moveToThread(this)**********/
InheritQThread(QObject *parent = Q_NULLPTR):QThread(parent){
        moveToThread(this); 
    }

/**************在InheritQThread::run函数添加exec()***************/
void run(){
    qDebug()<<"child thread = "<<QThread::currentThreadId();

    int i=0;
    m_flag = true;

    while(1)
    {
        ++i;

        emit ValueChanged(i);
        QThread::sleep(1);

        {
            QMutexLocker lock(&m_lock);
            if( !m_flag )
                break;
        }
    }
    
    exec(); //开启事件循环
    }

/************在InheritQThread类中添加QdebugSlot()槽函数***************/
public slots:
    void QdebugSlot(){
        qDebug()<<"QdebugSlot function is in thread:"<<QThread::currentThreadId();
    }

在MainWindow类中添加QdebugSignal信号;在构造函数中将QdebugSignal信号与InheritQThread::QdebugSlot槽函数进行绑;添加一个发送QdebugSignal信号的按钮:

/**********在MainWindow构造函数中绑定信号槽******************/
explicit MainWindow(QWidget *parent = nullptr) :
    QMainWindow(parent),
    ui(new Ui::MainWindow){

    qDebug()<<"GUI thread = "<<QThread::currentThreadId();

    ui->setupUi(this);
    WorkerTh = new InheritQThread(this);
    connect(WorkerTh, &InheritQThread::ValueChanged, this, &MainWindow::setValue);
    connect(this, &MainWindow::QdebugSignal, WorkerTh, &InheritQThread::QdebugSlot); //绑定信号槽
}

/********MainWindow类中添加信号QdebugSignal槽以及按钮事件槽函数**********/
signals:
    void QdebugSignal(); //添加QdebugSignal信号
private slots:
    //按钮的事件槽函数
    void on_SendQdebugSignalBt_clicked()
    {
        emit QdebugSignal();
    }

实现事件循环的程序已修改完成,来看下效果:

(1)在运行的时候为什么会出现以下警告?

QObject::moveToThread: Cannot move objects with a parent

我们看到MainWindow类中是这样定义InheritQThread类对象的:WorkerTh = new InheritQThread(this)。如果需要使用moveToThread()来改变对象的依附性,其创建时不能够带有父类。将语句改为:WorkerTh = new InheritQThread()即可。

(2)修改完成后,点击【start】启动线程,然后点击【stop】按钮跳出run函数中的while循环,最后点击【check thread state】按钮来检查线程的状态,会是什么样的情况呢?

由上图可以看到,线程依旧处于运行状态,这是因为run函数中调用了exec(),此时线程正处于事件循环中。

(3)接下来再点击【Send QdebugSignal】按钮来发送QdebugSignal信号。

由终端的打印信息得知,InheritQThread::QdebugSlot槽函数是在子线程中执行的。

3.1.4 子类化QThread线程的信号与槽

从上图可知,事件循环是一个无止尽循环,事件循环结束之前,exec()函数后的语句无法得到执行。只有槽函数所在线程开启了事件循环,它才能在对应信号发射后被调用。无论事件循环是否开启,信号发送后会直接进入槽函数所依附的线程的事件队列,然而,只有开启了事件循环,对应的槽函数才会在线程中得到调用。下面通过几种情况来验证下:

(1)代码和《3.1.3 使用事件循环》小节的代码一样,然后进行如下的操作:点击【start】按钮->再点击【Send QdebugSignal】按钮,这个时候槽函数会不会被执行呢?

这种情况无论点多少次发送QdebugSignal信号,InheritQThread::QdebugSlot槽函数都不会执行。因为当前线程还处于while循环当中,如果需要实现槽函数在当前线程中执行,那么当前线程就应该处于事件循环的状态,也就是正在执行exec()函数。所以如果需要InheritQThread::QdebugSlot槽函数执行,就需要点击【stop】按钮退出while循环,让线程进入事件循环。

(2)在《3.1.3 使用事件循环》小节的代码基础上,把InheritQThread::run函数删除,然后进行如下的操作:点击【start】启动线程->点击【stop】按钮跳出run函数中的while循环进入事件循环->点击【Send QdebugSignal】按钮来发送QdebugSignal信号,会有什么结果呢?

结果会和上面第一种情况一样,虽然信号已经在子线程的事件队列上,但是由于子线程没有事件循环,所以槽函数永远都不会被执行。

(3)在上面《3.1.3 使用事件循环》小节的代码基础上,将InheritQThread构造函数中的 moveToThread(this) 去除掉。进行如下操作:点击【start】启动线程->点击【stop】按钮跳出run函数中的while循环进入事件循环->点击【Send QdebugSignal】按钮来发送QdebugSignal信号,会有什么结果呢?

由上图可以看出InheritQThread::QdebugSlot槽函数居然是在GUI主线程中执行了。因为InheritQThread对象我们是在主线程中new出来的,如果不使用moveToThread(this)来改变对象的依附性关系,那么InheritQThread对象就是属于GUI主线程,根据connect信号槽的执行规则,最终槽函数会在对象所依赖的线程中执行。信号与槽绑定的connect函数的细节会在后面的《跨线程的信号槽》章节进行单独介绍。

3.1.5 如何正确退出线程并释放资源

InheritQThread类的代码不变动,和上述的代码一样:

#ifndef INHERITQTHREAD_H
#define INHERITQTHREAD_H
#include <QThread>
#include <QMutex>
#include <QMutexLocker>
#include <QDebug>

class InheritQThread:public QThread
{
    Q_OBJECT

public:
    InheritQThread(QObject *parent = Q_NULLPTR):QThread(parent){
        moveToThread(this);
    }

    void StopThread(){
        QMutexLocker lock(&m_lock);
        m_flag = false;
    }

protected:
    //线程执行函数
    void run(){
        qDebug()<<"child thread = "<<QThread::currentThreadId();

        int i=0;
        m_flag = true;

        while(1)
        {
            ++i;

            emit ValueChanged(i);
            QThread::sleep(1);

            {
                QMutexLocker lock(&m_lock);
                if( !m_flag )
                    break;
            }
        }

        exec();
    }

signals:
    void ValueChanged(int i);

public slots:
    void QdebugSlot(){
        qDebug()<<"QdebugSlot function is in thread:"<<QThread::currentThreadId();
    }

public:
    bool m_flag;
    QMutex m_lock;
};

#endif // INHERITQTHREAD_H

MainWindow类添加ExitBt、TerminateBt两个按钮,用于调用WorkerTh->exit(0)、WorkerTh->terminate()退出线程函数。由《2.5 QThread::quit()、QThread::exit()、QThread::terminate()源码》小节得知调用quit和exit是一样的,所以本处只添加了ExitBt按钮:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "ui_mainwindow.h"
#include "InheritQThread.h"
#include <QThread>
#include <QDebug>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr) :
        QMainWindow(parent),
        ui(new Ui::MainWindow){

        qDebug()<<"GUI thread = "<<QThread::currentThreadId();

        ui->setupUi(this);
        WorkerTh = new InheritQThread();
        connect(WorkerTh, &InheritQThread::ValueChanged, this, &MainWindow::setValue);

        connect(this, &MainWindow::QdebugSignal, WorkerTh, &InheritQThread::QdebugSlot);
    }

    ~MainWindow(){
        delete ui;
    }

signals:
    void QdebugSignal();

public slots:
    void setValue(int i){
        ui->lcdNumber->display(i);
    }

private slots:
    void on_startBt_clicked(){
        WorkerTh->start();
    }

    void on_stopBt_clicked(){
        WorkerTh->StopThread();
    }

    void on_checkBt_clicked(){
        if(WorkerTh->isRunning()){
            ui->label->setText("Running");
        }else{
            ui->label->setText("Finished");
        }
    }

    void on_SendQdebugSignalBt_clicked(){
        emit QdebugSignal();
    }

    void on_ExitBt_clicked(){
        WorkerTh->exit(0);
    }

    void on_TerminateBt_clicked(){
        WorkerTh->terminate();
    }

private:
    Ui::MainWindow *ui;
    InheritQThread *WorkerTh;
};

#endif // MAINWINDOW_H

运行上述的例程,点击【start】启动线程按钮,然后直接点击【exit(0)】或者【terminate()】,这样会直接退出线程吗?
点击【exit(0)】按钮(猛点)

点击【terminate()】按钮(就点一点)

由上述情况我们可以看到上面例程的线程启动之后,无论怎么点击【exit(0)】按钮,线程都不会退出,点击【terminate()】按钮的时候就会立刻退出当前线程。由《2.5 QThread::quit()、QThread::exit()、QThread::terminate()源码》小节可以得知,若使用QThread::quit()、QThread::exit()来退出线程,该线程就必须要在事件循环的状态(也就是正在执行exec()),线程才会退出。而QThread::terminate()不管线程处于哪种状态都会强制退出线程,但这个函数存在非常多不安定因素,不推荐使用。我们下面来看看如何正确退出线程。

(1)如何正确退出线程?

  • 如果线程内没有事件循环,那么只需要用一个标志变量来跳出run函数的while循环,这就可以正常退出线程了。
  • 如果线程内有事件循环,那么就需要调用QThread::quit()或者QThread::exit()来结束事件循环。像刚刚举的例程,不仅有while循环,循环后面又有exec(),那么这种情况就需要先让线程跳出while循环,然后再调用QThread::quit()或者QThread::exit()来结束事件循环。如下:

注意:尽量不要使用QThread::terminate()来结束线程,这个函数存在非常多不安定因素。

(2)如何正确释放线程资源?

退出线程不代表线程的资源就释放了,退出线程只是把线程停止了而已,那么QThread类或者QThread派生类的资源应该如何释放呢?直接 delete QThread类或者派生类的指针吗?当然不能这样做,千万别手动delete线程指针,手动delete会发生不可预料的意外。理论上所有QObject都不应该手动delete,如果没有多线程,手动delete可能不会发生问题,但是多线程情况下delete非常容易出问题,那是因为有可能你要删除的这个对象在Qt的事件循环里还排队,但你却已经在外面删除了它,这样程序会发生崩溃。 线程资源释放分为两种情况,一种是在创建QThread派生类时,添加了父对象,例如在MainWindow类中WorkerTh = new InheritQThread(this)让主窗体作为InheritQThread对象的父类;另一种是不设置任何父类,例如在MainWindow类中WorkerTh = new InheritQThread()。

  • 1、创建QThread派生类,有设置父类的情况:

这种情况,QThread派生类的资源都让父类接管了,当父对象被销毁时,QThread派生类对象也会被父类delete掉,我们无需显示delete销毁资源。但是子线程还没结束完,主线程就destroy掉了(WorkerTh的父类是主线程窗口,主线程窗口如果没等子线程结束就destroy的话,会顺手把WorkerTh也delete这时就会奔溃了)。 注意:这种情况不能使用moveToThread(this)改变对象的依附性。 因此我们应该把上面MainWindow类的构造函数改为如下:

~MainWindow(){
    WorkerTh->StopThread();//先让线程退出while循环
    WorkerTh->exit();//退出线程事件循环
    WorkerTh->wait();//挂起当前线程,等待WorkerTh子线程结束
    delete ui;
}
  • 2、创建QThread派生类,没有设置父类的情况:

也就是没有任何父类接管资源了,又不能直接delete QThread派生类对象的指针,但是QObject类中有 void QObject::deleteLater () [slot] 这个槽,这个槽非常有用,后面会经常用到它用于安全的线程资源销毁。我们通过以上的《2.3 QThreadPrivate::start()源码》小节可知线程结束之后会发出 QThread::finished() 的信号,我们将这个信号和 deleteLater 槽绑定,线程结束后调用deleteLater来销毁分配的内存。
在MainWindow类构造函数中,添加以下代码:

connect(WorkerTh, &QThread::finished, WorkerTh, &QObject::deleteLater) 

~MainWindow()析构函数可以把 wait()函数去掉了,因为该线程的资源已经不是让主窗口来接管了。当我们启动线程之后,然后退出主窗口或者直接点击【stop】+【exit()】按钮的时候,会出现以下的警告:

QThread::wait: Thread tried to wait on itself
QThread: Destroyed while thread is still running

为了让子线程能够响应信号并在子线程执行槽函数,我们在InheritQThread类构造函数中添加了 moveToThread(this) ,此方法是官方极其不推荐使用的方法。那么现在我们就遇到了由于这个方法引发的问题,我们把moveToThread(this)删除,程序就可以正常结束和释放资源了。那如果要让子线程能够响应信号并在子线程执行槽函数,这应该怎么做?在下面的章节会介绍一个官方推荐的《子类化QObject+moveToThread》的方法。

3.1.6 小结

  • QThread只有run函数是在新线程里;
  • 如果必须需要实现在线程内执行槽的情景,那就需要在QThread的派生类构造函数中调用moveToThread(this),并且在run函数内执行QThread::exec()开启事件循环;(极其不推荐使用moveToThread(this),下一节会介绍一种安全可靠的方法)
  • 若需要使用事件循环,需要在run函数中调用QThread::exec();
  • 尽量不要使用terminate()来结束线程,可以使用bool标志位退出或者在线程处于事件循环时调用QThread::quit、QThread::exit来退出线程;
  • 善用QObject::deleteLater来进行内存管理;
  • 在QThread执行start函数之后,run函数还未运行完毕,再次start,不会发生任何结果;
  • 子类化QThread多线程的方法适用于后台执行长时间的耗时操作、单任务执行的、无需在线程内执行槽的情景。

3.2 子类化QObject+moveToThread

从QThread源码可知,在Qt4.4之前,run 是纯虚函数,必须子类化QThread来实现run函数。而从Qt4.4开始,QThread不再支持抽象类,run 默认调用 QThread::exec() ,不需要子类化QThread,只需要子类化一个QObject,通过QObject::moveToThread将QObject派生类移动到线程中即可。这是官方推荐的方法,而且使用灵活、简单、安全可靠。如果线程要用到事件循环,使用继承QObject的多线程方法无疑是一个更好的选择。
这个小节主要是说一下,子类化QObject+moveToThread的多线程使用方法以及一些注意问题,其中有很多细节的问题其实和《3.1 子类化QThread》这个小节是一样的,在这里就不再多说了,不明白的可以到上一节找找答案。

3.2.1 步骤

  • 写一个继承QObject的类,将需要进行复杂耗时的逻辑封装到槽函数中,作为线程的入口,入口可以有多个;
  • 在旧线程创建QObject派生类对象和QThread对象,最好使用堆分配的方式创建(new),并且最好不要为此两个对象设置父类,便于后期程序的资源管理;
  • 把obj通过moveToThread方法转移到新线程中,此时obj不能有任何的父类;
  • 把线程的finished信号和obj对象、QThread对象的 QObject::deleteLater 槽连接,这个信号槽必须连接,否则会内存泄漏;如果QObject的派生类和QThread类指针是需要重复使用,那么就需要处理由对象被销毁之前立即发出的 QObject::destroyed 信号,将两个指针设置为nullptr,避免出现野指针;
  • 将其他信号与QObject派生类槽连接,用于触发线程执行槽函数里的任务;
  • 初始化完后调用 QThread::start() 来启动线程,默认开启事件循环;
  • 在逻辑结束后,调用 QThread::quit 或者 QThread::exit 退出线程的事件循环。

3.2.2 实例

写一个继承QObject的类:InheritQObject,代码如下:

#ifndef INHERITQOBJECT_H
#define INHERITQOBJECT_H

#include <QObject>
#include <QThread>
#include <QMutex>
#include <QMutexLocker>
#include <QDebug>

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

    }

    //用于退出线程循环计时的槽函数
    void StopTimer(){
        qDebug()<<"Exec StopTimer thread = "<<QThread::currentThreadId();
        QMutexLocker lock(&m_lock);
        m_flag = false;
    }

signals:
    void ValueChanged(int i);

public slots:
    void QdebugSlot(){
        qDebug()<<"Exec QdebugSlot thread = "<<QThread::currentThreadId();
    }

    //计时槽函数
    void TimerSlot(){
        qDebug()<<"Exec TimerSlot thread = "<<QThread::currentThreadId();

        int i=0;
        m_flag = true;

        while(1)
        {
            ++i;

            emit ValueChanged(i);
            QThread::sleep(1);

            {
                QMutexLocker lock(&m_lock);
                if( !m_flag )
                    break;
            }
        }
    }

private:
    bool m_flag;
    QMutex m_lock;
};

#endif // INHERITQOBJECT_H

mainwindow主窗口类,代码如下:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "ui_mainwindow.h"
#include "InheritQObject.h"
#include <QThread>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0) :
        QMainWindow(parent),
        ui(new Ui::MainWindow){

        qDebug()<<"GUI thread = "<<QThread::currentThreadId();

        ui->setupUi(this);

        //创建QThread线程对象以及QObject派生类对象,注意:都不需要设置父类
        m_th = new QThread();
        m_obj = new InheritQObject();
        
        //改变m_obj的线程依附关系
        m_obj->moveToThread(m_th);

        //释放堆空间资源
        connect(m_th, &QThread::finished, m_obj, &QObject::deleteLater);
        connect(m_th, &QThread::finished, m_th, &QObject::deleteLater);
        //设置野指针为nullptr
        connect(m_th, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);
        connect(m_obj, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);
        //连接其他信号槽,用于触发线程执行槽函数里的任务
        connect(this, &MainWindow::StartTimerSignal, m_obj, &InheritQObject::TimerSlot);
        connect(m_obj, &InheritQObject::ValueChanged, this, &MainWindow::setValue);
        connect(this, &MainWindow::QdebugSignal, m_obj, &InheritQObject::QdebugSlot);

        //启动线程,线程默认开启事件循环,并且线程正处于事件循环状态
        m_th->start();
    }

    ~MainWindow(){
        delete ui;
    }

signals:
    void StartTimerSignal();
    void QdebugSignal();

private slots:
    //触发线程执行m_obj的计时槽函数
    void on_startBt_clicked(){
        emit StartTimerSignal();
    }

    //退出计时槽函数
    void on_stopBt_clicked(){
        m_obj->StopTimer();
    }

    //检测线程状态
    void on_checkBt_clicked(){
        if(m_th->isRunning()){
            ui->label->setText("Running");
        }else{
            ui->label->setText("Finished");
        }
    }

    void on_SendQdebugSignalBt_clicked(){
        emit QdebugSignal();
    }

    //退出线程
    void on_ExitBt_clicked(){
        m_th->exit(0);
    }

    //强制退出线程
    void on_TerminateBt_clicked(){
        m_th->terminate();
    }

    //消除野指针
    void SetPtrNullptr(QObject *sender){
        if(qobject_cast<QObject*>(m_th) == sender){
            m_th = nullptr;
            qDebug("set m_th = nullptr");
        }

        if(qobject_cast<QObject*>(m_obj) == sender){
            m_obj = nullptr;
            qDebug("set m_obj = nullptr");
        }
    }

    //响应m_obj发出的信号来改变时钟
    void setValue(int i){
            ui->lcdNumber->display(i);
    }

private:
    Ui::MainWindow *ui;
    QThread *m_th;
    InheritQObject *m_obj;
};

#endif // MAINWINDOW_H

通过以上的实例可以看到,我们无需重写 QThread::run 函数,也无需显式调用 QThread::exec 来启动线程的事件循环了,通过QT源码可以知道,只要调用 QThread::start 它就会自动执行 QThread::exec 来启动线程的事件循环。
第一种多线程的创建方法(继承QThread的方法),如果run函数里面没有死循环也没有调用exec开启事件循环的话,就算调用了 QThread::start 启动线程,最终过一段时间,线程依旧是会退出,处于finished的状态。那么这种方式会出现这样的情况吗?我们直接运行上面的实例,然后过段时间检查线程的状态:

发现线程是一直处于运行状态的。那接下来我们说一下应该怎么正确使用这种方式创建的线程并正确退出线程和释放资源。

3.2.3 如何正确使用线程(信号槽)和创建线程资源

(1)如何正确使用线程?

如果需要让线程去执行一些行为,那就必须要正确使用信号槽的机制来触发槽函数,其他的方式调用槽函数都只是在旧线程中执行,无法达到预想效果。在多线程中信号槽的细节,会在《三、跨线程的信号槽》章节来讲解,这里我们先简单说如何使用信号槽来触发线程执行任务先。
通过以上的实例得知,MainWindow 构造函数中使用了connect函数将 StartTimerSignal() 信号和 InheritQObject::TimerSlot() 槽进行了绑定,代码语句如下:

connect(this, &MainWindow::StartTimerSignal, m_obj, &InheritQObject::TimerSlot);

当点击【startTime】按钮发出 StartTimerSignal() 信号时,这个时候就会触发线程去执行 InheritQObject::TimerSlot() 槽函数进行计时。

由上面的打印信息得知,InheritQObject::TimerSlot() 槽函数的确是在一个新的线程中执行了。在上面继承QThread的多线程方法中也有说到,在这个时候去执行QThread::exit或者是QThread::quit是无效的,退出的信号会一直挂在消息队列里,只有点击了【stopTime】按钮让线程退出 while 循环,并且线程进入到事件循环 ( exec() ) 中,才会生效,并退出线程。

如果将【startTime】按钮不是发出 StartTimerSignal() 信号,而是直接执行InheritQObject::TimerSlot() 槽函数,会是怎么样的结果呢?代码修改如下:

//触发线程执行m_obj的计时槽函数
void on_startBt_clicked(){
    m_obj->TimerSlot();
}

我们会发现界面已经卡死,InheritQObject::TimerSlot() 槽函数是在GUI主线程执行的,这就导致了GUI界面的事件循环无法执行,也就是界面无法被更新了,所以出现了卡死的现象。所以要使用信号槽的方式来触发线程工作才是有效的,不能够直接调用obj里面的成员函数。

(2)如何正确创建线程资源?

有一些资源我们可以直接在旧线程中创建(也就是不通过信号槽启动线程来创建资源),在新线程也可以直接使用,例如实例中的bool m_flag和QMutex m_lock变量都是在就线程中定义的,在新线程也可以使用。但是有一些资源,如果你需要在新线程中使用,那么就必须要在新线程创建,例如定时器、网络套接字等,下面以定时器作为例子,代码按照下面修改:

/**********在InheritQObject类中添加QTimer *m_timer成员变量*****/
QTimer *m_timer;

/**********在InheritQObject构造函数创建QTimer实例*****/
m_timer = new QTimer();

/**********在InheritQObject::TimerSlot函数使用m_timer*****/
m_timer->start(1000);

运行点击【startTime】按钮的时候,会出现以下报错:

QObject::startTimer: Timers cannot be started from another thread

由此可知,QTimer是不可以跨线程使用的,所以将程序修改成如下,将QTimer的实例创建放到线程里面创建:

/*********在InheritQObject类中添加Init的槽函数,将需要初始化创建的资源放到此处********/
public slots:
    void Init(){
        m_timer = new QTimer();
    }
    
/********在MainWindow类中添加InitSiganl()信号,并绑定信号槽***********/
//添加信号
signals:
    void InitSiganl();
    
//在MainWindow构造函数添加以下代码
connect(this, &MainWindow::InitSiganl, m_obj, &InheritQObject::Init); //连接信号槽
emit InitSiganl(); //发出信号,启动线程初始化QTimer资源

这样QTimer定时器就属于新线程,并且可以正常使用啦。网络套接字QUdpSocket、QTcpSocket等资源同理处理就可以了。

3.2.4 如何正确退出线程并释放资源

(1)如何正确退出线程?

正确退出线程的方式,其实和上面《3.1.5 如何正确退出线程并释放资源》小节所讲到的差不多,就是要使用 quit 和 exit 来退出线程,避免使用 terminate 来强制结束线程,有时候会出现异常的情况。例如以上的实例,启动之后,直接点击 【terminate】按钮,界面就会出现卡死的现象。

(2)如何正确释放线程资源?

在上面《3.1.5 如何正确退出线程并释放资源》小节也有讲到,千万别手动delete QThread类或者派生类的线程指针,手动delete会发生不可预料的意外。理论上所有QObject都不应该手动delete,如果没有多线程,手动delete可能不会发生问题,但是多线程情况下delete非常容易出问题,那是因为有可能你要删除的这个对象在Qt的事件循环里还排队,但你却已经在外面删除了它,这样程序会发生崩溃。所以需要 善用QObject::deleteLater 和 QObject::destroyed来进行内存管理。如上面实例使用到的代码:

//释放堆空间资源
connect(m_th, &QThread::finished, m_obj, &QObject::deleteLater);
connect(m_th, &QThread::finished, m_th, &QObject::deleteLater);
//设置野指针为nullptr
connect(m_th, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);
connect(m_obj, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);

//消除野指针
void SetPtrNullptr(QObject *sender){
    if(qobject_cast<QObject*>(m_th) == sender){
        m_th = nullptr;
        qDebug("set m_th = nullptr");
    }

    if(qobject_cast<QObject*>(m_obj) == sender){
        m_obj = nullptr;
        qDebug("set m_obj = nullptr");
    }
}

当我们调用线程的 quit 或者 exit 函数,并且线程到达了事件循环的状态,那么线程就会在结束并且发出 QThread::finished 的信号来触发 QObject::deleteLater 槽函数,QObject::deleteLater就会销毁系统为m_obj、m_th对象分配的资源。这个时候m_obj、m_th指针就属于野指针了,所以需要根据QObject类或者QObject派生类对象销毁时发出来的 QObject::destroyed 信号来设置m_obj、m_th指针为nullptr,避免野指针的存在。
运行上面的实例,然后点击【exit】按钮,结果如下图:

3.2.5 小结

  • 这种QT多线程的方法,实现简单、使用灵活,并且思路清晰,相对继承于QThread类的方式更有可靠性,这种方法也是官方推荐的实现方法。如果线程要用到事件循环,使用继承QObject的多线程方法无疑是一个更好的选择;
  • 创建QObject派生类对象不能带有父类;
  • 调用QThread::start是默认启动事件循环;
  • 必须需要使用信号槽的方式使用线程;
  • 需要注意跨线资源的创建,例如QTimer、QUdpSocket等资源,如果需要在子线程中使用,必须要在子线程创建;
  • 要善用QObject::deleteLater 和 QObject::destroyed来进行内存管理 ;
  • 尽量避免使用terminate强制退出线程,若需要退出线程,可以使用quit或exit;

3.3 继承QRunnable+QThreadPool

此方法个人感觉使用的相对较少,在这里只是简单介绍下使用的方法。我们可以根据使用的场景来选择方法。

此方法和QThread的区别:

  • 与外界通信方式不同。由于QThread是继承于QObject的,但QRunnable不是,所以在QThread线程中,可以直接将线程中执行的结果通过信号的方式发到主程序,而QRunnable线程不能用信号槽,只能通过别的方式,等下会介绍;
  • 启动线程方式不同。QThread线程可以直接调用start()函数启动,而QRunnable线程需要借助QThreadPool进行启动;
  • 资源管理不同。QThread线程对象需要手动去管理删除和释放,而QRunnable则会在QThreadPool调用完成后自动释放。

接下来就来看看QRunnable的用法、使用场景以及注意事项;

3.3.1 步骤

要使用QRunnable创建线程,步骤如下:

  • 继承QRunnable。和QThread使用一样, 首先需要将你的线程类继承于QRunnable;
  • 重写run函数。还是和QThread一样,需要重写run函数;
  • 使用QThreadPool启动线程。

3.3.2 实例

继承于QRunnable的类:

#ifndef INHERITQRUNNABLE_H
#define INHERITQRUNNABLE_H

#include <QRunnable>
#include <QWidget>
#include <QDebug>
#include <QThread>

class CusRunnable : public QRunnable
{
public:
    explicit CusRunnable(){
    }

    ~CusRunnable(){
        qDebug() << __FUNCTION__;
    }

    void run(){
        qDebug() << __FUNCTION__ << QThread::currentThreadId();
        QThread::msleep(1000);
    }
};

#endif // INHERITQRUNNABLE_H

主界面类:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "ui_mainwindow.h"
#include "InheritQRunnable.h"
#include <QThreadPool>
#include <QDebug>

namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

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

        m_pRunnable = new CusRunnable();
        qDebug() << __FUNCTION__  << QThread::currentThreadId();
        QThreadPool::globalInstance()->start(m_pRunnable);
    }

    ~MainWindow(){
        qDebug() << __FUNCTION__ ;
        delete ui;
    }

private:
    Ui::MainWindow *ui;
    CusRunnable * m_pRunnable = nullptr;
};

#endif // MAINWINDOW_H

直接运行以上实例,结果输出如下:

MainWindow 0x377c
run 0x66ac
~CusRunnable

我们可以看到这里打印的线程ID是不同的,说明是在不同线程中执行,而线程执行完后就自动进入到析构函数中, 不需要手动释放。

3.3.3 启动线程的方式

上面我们说到要启动QRunnable线程,需要QThreadPool配合使用,而调用方式有两种:全局线程池和非全局线程池。

(1)使用全局线程池启动

QThreadPool::globalInstance()->start(m_pRunnable);

(2)使用非全局线程池启动

该方式可以控制线程最大数量, 以及其他设置,比较灵活,具体参照帮助文档。

QThreadPool	  threadpool;
threadpool.setMaxThreadCount(1);
threadpool.start(m_pRunnable);

3.3.4 如何与外界通信

前面我们提到,因为QRunnable没有继承于QObject,所以没法使用信号槽与外界通信,那么,如果要在QRunnable线程中和外界通信怎么办呢,通常有两种做法:

  • 使用多继承。让我们的自定义线程类同时继承于QRunnable和QObject,这样就可以使用信号和槽,但是多线程使用比较麻烦,特别是继承于自定义的类时,容易出现接口混乱,所以在项目中尽量少用多继承。
  • 使用QMetaObject::invokeMethod。

接下来只介绍使用QMetaObject::invokeMethod来通信:

QMetaObject::invokeMethod 函数定义如下:

static bool QMetaObject::invokeMethod(
                         QObject *obj, const char *member,
                         Qt::ConnectionType,
                         QGenericReturnArgument ret,
                         QGenericArgument val0 = QGenericArgument(Q_NULLPTR),
                         QGenericArgument val1 = QGenericArgument(),
                         QGenericArgument val2 = QGenericArgument(),
                         QGenericArgument val3 = QGenericArgument(),
                         QGenericArgument val4 = QGenericArgument(),
                         QGenericArgument val5 = QGenericArgument(),
                         QGenericArgument val6 = QGenericArgument(),
                         QGenericArgument val7 = QGenericArgument(),
                         QGenericArgument val8 = QGenericArgument(),
                         QGenericArgument val9 = QGenericArgument());

该函数就是尝试调用obj的member函数,可以是信号、槽或者Q_INVOKABLE声明的函数(能够被Qt元对象系统唤起),只需要将函数的名称传递给此函数,调用成功返回true,失败返回false。member函数调用的返回值放在ret中,如果调用是异步的,则不能计算返回值。你可以将最多10个参数(val0、val1、val2、val3、val4、val5、val6、val7、val8和val9)传递给member函数,必须使用Q_ARG()和Q_RETURN_ARG()宏封装参数,Q_ARG()接受类型名 + 该类型的常量引用;Q_RETURN_ARG()接受一个类型名 + 一个非常量引用。

QMetaObject::invokeMethod可以是异步调用,也可以是同步调用。这取决与它的连接方式Qt::ConnectionType type:

  • 如果类型是Qt::DirectConnection,则会立即调用该成员,同步调用。
  • 如果类型是Qt::QueuedConnection,当应用程序进入主事件循环时,将发送一个QEvent并调用该成员,异步调用。
  • 如果类型是Qt::BlockingQueuedConnection,该方法将以与Qt::QueuedConnection相同的方式调用,不同的地方:当前线程将阻塞,直到事件被传递。使用此连接类型在同一线程中的对象之间通信将导致死锁。
  • 如果类型是Qt::AutoConnection,如果obj与调用者在同一线程,成员被同步调用;否则,它将异步调用该成员。

我们在主界面中定一个函数,用于更新界面内容:

Q_INVOKABLE void setText(QString msg){
    ui->label->setText(msg);
}

继承于QRunnable的线程类,修改完成如下:

#ifndef INHERITQRUNNABLE_H
#define INHERITQRUNNABLE_H

#include <QRunnable>
#include <QWidget>
#include <QDebug>
#include <QThread>

class CusRunnable : public QRunnable
{
public:
    //修改构造函数
    explicit CusRunnable(QObject *obj):m_pObj(obj){
    }

    ~CusRunnable(){
        qDebug() << __FUNCTION__;
    }

    void run(){
        qDebug() << __FUNCTION__ << QThread::currentThreadId();
        QMetaObject::invokeMethod(m_pObj,"setText",Q_ARG(QString,"hello world!")); //此处与外部通信
        QThread::msleep(1000);
    }

private:
    QObject * m_pObj = nullptr; //定义指针
};

#endif // INHERITQRUNNABLE_H

创建线程对象时,需要将主界面对象传入线程类,如下:

m_pRunnable = new CusRunnable(this);

到这里也就实现了线程与外部通信了,运行效果如下:

3.3.5 小结

  • 使用该方法实现的多线程,线程中的资源无需用户手动释放,线程执行完后会自动回收资源;
  • 和继承QThread的方法一样需要继承类,并且重新实现run函数;
  • 需要结合QThreadPool线程池来使用;
  • 与外界通信可以使用如果使用信号槽机制会比较麻烦,可以使用QMetaObject::invokeMethod的方式与外界通信。

3.4 QtConcurrent::run()+QThreadPool

在QT开发的场景中,个人觉得此方法使用的也比较少,所以本文只作一个简单使用的介绍。QtConcurrent 是命名空间 (namespace),它提供了高层次的函数接口 (APIs),使所写程序,可根据计算机的 CPU 核数,自动调整运行的线程数目。本文以 Qt 中的 QtConcurrent::run() 函数为例,介绍如何将函数运行在单独的线程中。

(1)使用 QtConcurrent 模块,需要在 .pro 中添加:

QT += concurrent

(2)将一个普通函数运行在单独线程:

#include <QApplication>
#include <QDebug>
#include <QThread>
#include <QtConcurrent>

void fun1(){
    qDebug()<<__FUNCTION__<<QThread::currentThread();
}

void fun2(QString str1, QString str2){
    qDebug()<<__FUNCTION__<<str1+str2<<QThread::currentThread();
}

int fun3(int i, int j){
    qDebug()<<__FUNCTION__<<QThread::currentThread();
    return i+j;
}

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

    qDebug()<<__FUNCTION__<<QThread::currentThread();

    //无参数的普通函数
    QFuture<void> fut1 = QtConcurrent::run(fun1);

    //有参数的普通函数
    QFuture<void> fut2 = QtConcurrent::run(fun2, QString("Thread"),QString(" 2"));

    //获取普通函数的返回值
    int i=1, j=2;
    QFuture<int> fut3 = QtConcurrent::run(fun3, i, j);
    qDebug()<<"ret:"<<fut3.result();

    //以上的例子,如果要为其指定线程池,可以将线程池的指针作为第一个参数传递进去
    QThreadPool pool;
    QFuture<void> fut4 = QtConcurrent::run(&pool, fun1);

    fut1.waitForFinished();
    fut2.waitForFinished();
    fut3.waitForFinished();
    fut4.waitForFinished();

    return a.exec();
}

输出结果:

qMain QThread(0xf380590)
fun2 "Thread 2" QThread(0x1ca7c758, name = "Thread (pooled)")
fun1 QThread(0x1ca7c6d8, name = "Thread (pooled)")
fun3 QThread(0x1ca7c5b8, name = "Thread (pooled)")
ret: 3
fun1 QThread(0x1ca7c438, name = "Thread (pooled)")

(3)将类中的成员函数单独运行在线程中:

将类中的成员函数运行在某一个线程中,可将指向该类实例的引用或指针作为 QtConcurrent::run 的第一个参数传递进去,常量成员函数一般传递常量引用 (const reference),而非常量成员函数一般传递指针 (pointer)。

  • 常量成员函数

在一个单独的线程中,调用 QByteArray 的常量成员函数 split(),传递给 run() 函数的参数是 bytearray

//常量成员函数QByteArray::split()
QByteArray bytearray = "hello,world";
QFuture<QList<QByteArray> > future = QtConcurrent::run(bytearray, &QByteArray::split, ',');
QList<QByteArray> result = future.result();
qDebug()<<"result:"<<result;
  • 非常量成员函数

在一个单独的线程中,调用 QImage 的非常量成员函数 invertPixels(),传递给 run() 函数的参数是 &image

QImage image = ...;
QFuture<void> future = QtConcurrent::run(&image, &QImage::invertPixels, QImage::InvertRgba);
...
future.waitForFinished();  // At this point, the pixels in 'image' have been inverted

四、跨线程的信号槽

线程的信号槽机制需要开启线程的事件循环机制,即调用QThread::exec()函数开启线程的事件循环。

Qt信号-槽连接函数原型如下:

bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection ) 

Qt支持5种连接方式

  • Qt::DirectConnection(直连方式)(信号与槽函数关系类似于函数调用,同步执行)
    当信号发出后,相应的槽函数将立即被调用。emit语句后的代码将在所有槽函数执行完毕后被执行。当信号发射时,槽函数将直接被调用。无论槽函数所属对象在哪个线程,槽函数都在发射信号的线程内执行。
  • Qt::QueuedConnection(队列方式)(此时信号被塞到事件队列里,信号与槽函数关系类似于消息通信,异步执行)
    当信号发出后,排队到信号队列中,需等到接收对象所属线程的事件循环取得控制权时才取得该信号,调用相应的槽函数。emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕。当控制权回到接收者所依附线程的事件循环时,槽函数被调用。槽函数在接收者所依附线程执行。
  • Qt::AutoConnection(自动方式) 
    Qt的默认连接方式,如果信号的发出和接收信号的对象同属一个线程,那个工作方式与直连方式相同;否则工作方式与队列方式相同。如果信号在接收者所依附的线程内发射,则等同于直接连接如果发射信号的线程和接受者所依附的线程不同,则等同于队列连接
  • Qt::BlockingQueuedConnection(信号和槽必须在不同的线程中,否则就产生死锁) 
    槽函数的调用情形和Queued Connection相同,不同的是当前的线程会阻塞住,直到槽函数返回。
  • Qt::UniqueConnection
    与默认工作方式相同,只是不能重复连接相同的信号和槽,因为如果重复连接就会导致一个信号发出,对应槽函数就会执行多次。

如果没有特殊的要求我们connect函数选择默认的连接方式就好,也就是connect的第五个参数不填写就ok,例如:

connect(m_obj, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);

五、总结

本文章分析了部分QThread源码,讲解了四种QT多线程的实现方法,以及多线程信号槽连接的知识点。接下来我再简单对以上四种QT多线程的实现方法,总结一下哪种情况该使用哪种 Qt 线程技术:

需要线程的生命周期 开发场景 解决方案
单次调用 在其他的线程中运行一个方法,当方法运行结束后退出线程。 (1)编写一个函数,然后利用 QtConcurrent::run()运行它;(2)从QRunnable 派生一个类,并利用全局线程池QThreadPool::globalInstance()->start()来运行它。(3) 从QThread派生一个类, 重载QThread::run() 方法并使用QThread::start()来运行它。
单次调用 一个耗时的操作必须放到另一个线程中运行。在这期间,状态信息必须发送到GUI线程中。 使用 QThread,,重载run方法并根据情况发送信号。.使用queued信号/槽连接来连接信号与GUI线程的槽。
常驻 有一对象位于另一个线程中,将让其根据不同的请求执行不同的操作。这意味与工作者线程之间的通信是必须的。 从QObject 派生一个类并实现必要的槽和信号,将对象移到一个具有事件循环的线程中,并通过queued信号/槽连接与对象进行通信。

当然QT还有其他实现多线程的方法,例如使用QtConcurrent::map()函数、QSocketNotifier,具体怎么使用,这里就不再过多介绍了。

这个是本文章实例的源码地址:https://gitee.com/CogenCG/QThreadExample.git

posted @ 2020-11-13 08:52  李春港  阅读(5719)  评论(5编辑  收藏  举报