【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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM