1. 程式人生 > >Qt建立多執行緒的兩種方法

Qt建立多執行緒的兩種方法

1.摘要

Qt有兩種多執行緒的方法,其中一種是繼承QThread的run函式,另外一種是把一個繼承於QObject的類轉移到一個Thread裡。 Qt4.8之前都是使用繼承QThread的run這種方法,但是Qt4.8之後,Qt官方建議使用第二種方法。兩種方法區別不大,用起來都比較方便,但繼承QObject的方法更加靈活。這裡要記錄的是如何正確的建立一個執行緒,特別是如何正確的退出一個執行緒。

本文先介紹QThread的普通用法,這個用法可能網上很多文章都介紹過,如果已經瞭解大可跳過此節,本文重點介紹執行緒退出的幾種方法,根據需求正確的建立和退出執行緒等問題。

2.Qt多執行緒方法1 繼承QThread

在使用繼承QThreadrun方法之前需要了解一條規則:

QThread只有run函式是在新執行緒裡的,其他所有函式都在QThread生成的執行緒裡

QThread只有run函式是在新執行緒裡的 

QThread只有run函式是在新執行緒裡的 

QThread只有run函式是在新執行緒裡的

重要的事情說3遍!!!

如果QThread是在ui所在的執行緒裡生成,那麼QThread的其他非run函式都是和ui執行緒一樣的,所以,QThread的繼承類的其他函式儘量別要有太耗時的操作,要確保所有耗時的操作都在run函式裡。 在UI執行緒下呼叫QThread

的非run函式(其實也不應該直接呼叫run函式,而應該使用start函式),和執行普通函式無區別,這時,如果這個函式要對QThread的某個變數進行變更,而這個變數在run函式裡也會被用到,這時就需要注意加鎖的問題,因為可能這個變數前幾毫秒剛剛在run中呼叫,再呼叫時已經被另外的執行緒修改了。

2.1寫一個繼承於QThread的執行緒

本文的重點不是教會你繼承run寫一個多執行緒,任何有程式設計基礎的5分鐘就能學會使用QThread的方法,本文真正要講的是後面那幾節,如如何安全的退出一個執行緒,如何開啟一個臨時執行緒,執行結束馬上關閉等問題。如果你對QThread有初步瞭解,那麼可以略過這節,但你最好看看這節後面提出的幾個問題。

任何繼承於QThread的執行緒都是通過繼承QThreadrun函式來實現多執行緒的,因此,必須重寫QThreadrun函式,把複雜邏輯寫在QThreadrun函式中。

看看一個普通繼承QThread的例子: 標頭檔案:

#ifndef THREADFROMQTHREAD_H
#define THREADFROMQTHREAD_H
#include <QThread>
 
class ThreadFromQThread : public QThread
{
    Q_OBJECT
signals:
    void message(const QString& info);
    void progress(int present);
public:
    ThreadFromQThread(QObject* par);
    ~ThreadFromQThread();
    void setSomething();
    void getSomething();
    void setRunCount(int count);
    void run();
    void doSomething();
private:
    int m_runCount;
};
 
#endif // THREADFROMQTHREAD_H

cpp檔案:

 
#include "ThreadFromQThread.h"
#include <QDebug>
ThreadFromQThread::ThreadFromQThread(QObject* par) : QThread(par)
,m_runCount(20)
{
 
}
 
ThreadFromQThread::~ThreadFromQThread()
{
    qDebug() << "ThreadFromQThread::~ThreadFromQThread()";
}
 
void ThreadFromQThread::setSomething()
{
    msleep(500);
    QString str = QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId());
    emit message(str);
}
 
void ThreadFromQThread::getSomething()
{
    msleep(500);
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
}
 
void ThreadFromQThread::setRunCount(int count)
{
    m_runCount = count;
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
}
 
void ThreadFromQThread::run()
{
    int count = 0;
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
    emit message(str);
    while(1)
    {
        sleep(1);
        ++count;
        emit progress(((float)count / m_runCount) * 100);
        emit message(QString("ThreadFromQThread::run times:%1").arg(count));
        doSomething();
        if(m_runCount == count)
        {
            break;
        }
    }
}
 
void ThreadFromQThread::doSomething()
{
    msleep(500);
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));    
}

這個簡單的例子有一個Qt類常見的內容,包含了普通方法,訊號槽,和一個run函式。這裡函式setSomething();進行了500ms的延遲,getSomething同理。這是為了驗證在QThread::run()之外呼叫QThread成員函式不會執行在新執行緒裡。

上面程式碼用到了QThread::currentThreadId()這是一個靜態函式,用於返回當前執行緒控制代碼,這個值除了區分以外沒有別的用處。

為了驗證這個執行緒,編寫一個簡單的介面,這個介面主要用於驗證如下幾個問題:、

  • 在UI執行緒呼叫setSomething();函式和getSomething();函式會不會卡頓?
  • 在UI執行緒呼叫QThread::quit()QThread::exit()函式會不會停止執行緒?
  • 在UI執行緒呼叫QThread::terminate函式會不會停止執行緒?
  • 如何正確的退出執行緒?

2.2 QThread的幾個函式quit、exit、terminate函式

為了驗證上面這些,編寫一個簡單的介面如下圖所示:

 
#include "Widget.h"
#include "ui_Widget.h"
#include "ThreadFromQThread.h"
#include <QDebug>
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //控制元件初始化
    ui->progressBar->setRange(0,100);
    ui->progressBar->setValue(0);
    ui->progressBar_heart->setRange(0,100);
    ui->progressBar_heart->setValue(0);
    //按鈕的訊號槽關聯
    connect(ui->pushButton_qthread1,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadClicked);
    connect(ui->pushButton_qthread1_setSomething,&QPushButton::clicked
            ,this,&Widget::onButtonQthread1SetSomethingClicked);
    connect(ui->pushButton_qthread1_getSomething,&QPushButton::clicked
            ,this,&Widget::onButtonQthread1GetSomethingClicked);
    connect(ui->pushButton_qthreadQuit,&QPushButton::clicked
            ,this,&Widget::onButtonQthreadQuitClicked);
    connect(ui->pushButton_qthreadTerminate,&QPushButton::clicked
            ,this,&Widget::onButtonQthreadTerminateClicked);
    connect(ui->pushButton_qthreadExit,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadExitClicked);
    connect(ui->pushButton_doSomthing,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadDoSomthingClicked);
    connect(ui->pushButton_qthreadRunLocal,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadRunLoaclClicked);
    //
    connect(ui->pushButton_qobjectStart,&QPushButton::clicked
            ,this,&Widget::onButtonObjectMove2ThreadClicked);
    connect(ui->pushButton_objQuit,&QPushButton::clicked
            ,this,&Widget::onButtonObjectQuitClicked);
    //
    connect(&m_heart,&QTimer::timeout,this,&Widget::heartTimeOut);
    m_heart.setInterval(100);
    //全域性執行緒的建立
    m_thread = new ThreadFromQThread(this);
    connect(m_thread,&ThreadFromQThread::message
            ,this,&Widget::receiveMessage);
    connect(m_thread,&ThreadFromQThread::progress
            ,this,&Widget::progress);
    connect(m_thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);
 
    m_heart.start();
}
 
 
 
Widget::~Widget()
{
    qDebug() << "start destroy widget";
    m_thread->stopImmediately();//由於此執行緒的父物件是Widget,因此退出時需要進行判斷
    m_thread->wait();
    delete ui;
    qDebug() << "end destroy widget";
}
 
void Widget::onButtonQThreadClicked()
{
    ui->progressBar->setValue(0);
    if(m_thread->isRunning())
    {
        return;
    }
    m_thread->start();
}
 
void Widget::progress(int val)
{
    ui->progressBar->setValue(val);
}
 
void Widget::receiveMessage(const QString &str)
{
    ui->textBrowser->append(str);
}
 
void Widget::heartTimeOut()
{
    static int s_heartCount = 0;
    ++s_heartCount;
    if(s_heartCount > 100)
    {
        s_heartCount = 0;
    }
    ui->progressBar_heart->setValue(s_heartCount);
}
 
void Widget::onButtonQthread1SetSomethingClicked()
{
    m_thread->setSomething();
}
 
void Widget::onButtonQthread1GetSomethingClicked()
{
    m_thread->getSomething();
}
 
void Widget::onButtonQthreadQuitClicked()
{
    ui->textBrowser->append("m_thread->quit() but not work");
    m_thread->quit();
}
 
void Widget::onButtonQthreadTerminateClicked()
{
    m_thread->terminate();
}
 
void Widget::onButtonQThreadDoSomthingClicked()
{
    m_thread->doSomething();
}
 
void Widget::onButtonQThreadExitClicked()
{
    m_thread->exit();
}
 
void Widget::onQThreadFinished()
{
    ui->textBrowser->append("ThreadFromQThread finish");
}

介面為上面提到的幾個問題提供了按鈕, 介面有一個心跳進度條,它是主程式的定時器控制,每100ms觸發用於證明主程式的ui執行緒沒有卡死。第二個進度條由執行緒控制。

點選"QThread run"按鈕,觸發onButtonQThreadClicked槽,子執行緒會執行,子執行緒執行起來後,會列印

../QtThreadTest/ThreadFromQThread.cpp->run,thread id:2900388672

可以確定執行緒執行的id是2900388672 子執行緒是個迴圈,每次迴圈都會有列印資訊:

ThreadFromQThread::run times:1 doSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:2900388672 ThreadFromQThread::run times:2 doSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:2900388672

doSomething是在run函式裡呼叫,其執行緒id是2900388672,可見這時doSomething函式是執行在子執行緒裡的。

這時,我在介面點選getSomething,setSomething,doSomething會列印:

getSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:3021526784 setSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:3021526784 doSomething->../QtThreadTest/ThreadFromQThread.cpp,thread id:3021526784

說明在非run函式裡呼叫QThread的成員函式,並不是線上程裡執行(3021526784是widget所線上程)

這時我點選quit,thread並沒進行任何處理,QThread在不呼叫exec()情況下是exit函式和quit函式是沒有作用的。

m_thread->quit() but not work

點選terminate按鈕,執行緒馬上終止,列印:

ThreadFromQThread finish

動態圖如下圖所示:

因此可以看出quitexit函式都不會中途終端執行緒,要馬上終止一個執行緒可以使用terminate函式,但這個函式存在非常不安定因素,不推薦使用。那麼如何安全的終止一個執行緒呢?

2.3 正確的終止一個執行緒

最簡單的方法是新增一個bool變數,通過主執行緒修改這個bool變數來進行終止,但這樣有可能引起訪問衝突,需要加鎖 我們需要在原來的標頭檔案加上如下語句:

 
#include <QMutex>
 
class ThreadFromQThread : public QThread
{
...........
public slots:
    void stopImmediately();
private:
    QMutex m_lock;
    bool m_isCanRun;
.........
};

run函式需要進行修改:

void ThreadFromQThread::stopImmediately()
{
    QMutexLocker locker(&m_lock);
    m_isCanRun = false;
}
 
void ThreadFromQThread::run()
{
    int count = 0;
    m_isCanRun = true;//標記可以執行
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((unsigned int)QThread::currentThreadId());
    emit message(str);
    while(1)
    {
        sleep(1);
        ++count;
        emit progress(((float)count / m_runCount) * 100);
        emit message(QString("ThreadFromQThread::run times:%1").arg(count));
        doSomething();
        if(m_runCount == count)
        {
            break;
        }
 
        {
            QMutexLocker locker(&m_lock);
            if(!m_isCanRun)//在每次迴圈判斷是否可以執行,如果不行就退出迴圈
            {
                return;
            }
        }
    }
}

QMutexLocker可以安全的使用QMutex,以免忘記解鎖(有點類似std::unique_ptr),這樣每次迴圈都會看看是否要馬上終止。 線上程需要馬上退出時,可以在外部呼叫stopImmediately()函式終止執行緒,之前的例子可以知道,由於在主執行緒呼叫QThread非run()函式的函式都是在主執行緒執行,因此,在主執行緒呼叫類似m_thread->stopImmediately()會幾乎馬上把執行緒的成員變數m_isCanRun設定為false(面對多執行緒問題要用面向過程的思維思考),因此在子執行緒的run函式的迴圈中遇到m_isCanRun的判斷後就會退出run函式,繼承QThread的函式在執行完run函式後就視為執行緒完成,會發射finish訊號。

2.4 如何正確啟動一個執行緒

執行緒的啟動有幾種方法,這幾種方法設計到它的父物件歸屬問題,和如何刪除他的問題。首先要搞清楚這個執行緒是否和UI的生命週期一致,直到UI結束執行緒才結束,還是這個執行緒只是臨時生成,等計算完成就銷燬。

第一種情況的執行緒在建立時會把生成執行緒的窗體作為它的父物件,這樣窗體結束時會自動析構執行緒的物件。但這時候要注意一個問題,就是窗體結束時執行緒還未結束如何處理,如果沒有處理這種問題,你會發現關閉視窗時會導致程式崩潰。往往這種執行緒是一個監控執行緒,如監控某個埠的執行緒。為了好區分,暫時叫這種叫全域性執行緒,它在UI的生命週期中都存在。

第二種情況是一種臨時執行緒,這種執行緒一般是突然要處理一個大計算,為了不讓UI假死需要觸發的執行緒,這時需要注意一個問題,就是線上程還沒計算完成,使用者突然終止或變更時如何處理,這種執行緒往往更多見且更容易出錯,如開啟一個大檔案,顯示一個大圖片,使用者可能看一個大圖片還沒等圖片處理完成又切換到下一個圖片,這時繪圖執行緒要如何處理才能順利解決?為了好區分,暫時叫這種叫區域性執行緒,它在UI的生命週期中僅僅是某時刻才會觸發,然後銷燬。

這就涉及到如何終止正在執行的執行緒這個問題!

2.4.1正確的啟動一個全域性執行緒(和UI一直存在的執行緒)

我發現大部分網上的教程都是教你建立一個全域性的執行緒,但往往這種執行緒用的不多,也比較好管理,需要注意的是程式退出時對執行緒的處理問題。 在ui的標頭檔案中宣告一個執行緒的指標

widget.h:

ThreadFromQThread* m_thread;

wodget.cpp:

class Widget : public QWidget
{
    Q_OBJECT
 
public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();
private slots:
    void onButtonQThreadClicked();
    void onButtonQthread1SetSomethingClicked();
    void onButtonQthread1GetSomethingClicked();
    void onButtonQthreadQuitClicked();
    void onButtonQthreadTerminateClicked();
    void onButtonQThreadDoSomthingClicked();
    void onQThreadFinished();
......
    void progress(int val);
    void receiveMessage(const QString& str);
    void heartTimeOut();
private:
    Ui::Widget *ui;
    ThreadFromQThread* m_thread;
    QTimer m_heart;
......
};

先看窗體生成的建構函式

 
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
  ,m_objThread(NULL)
{
   
   ui->setupUi(this);
    //控制元件初始化
    ui->progressBar->setRange(0,100);
    ui->progressBar->setValue(0);
    ui->progressBar_heart->setRange(0,100);
    ui->progressBar_heart->setValue(0);
    //按鈕的訊號槽關聯
    connect(ui->pushButton_qthread1,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadClicked);
    connect(ui->pushButton_qthread1_setSomething,&QPushButton::clicked
            ,this,&Widget::onButtonQthread1SetSomethingClicked);
    connect(ui->pushButton_qthread1_getSomething,&QPushButton::clicked
            ,this,&Widget::onButtonQthread1GetSomethingClicked);
    connect(ui->pushButton_qthreadQuit,&QPushButton::clicked
            ,this,&Widget::onButtonQthreadQuitClicked);
    connect(ui->pushButton_qthreadTerminate,&QPushButton::clicked
            ,this,&Widget::onButtonQthreadTerminateClicked);
    connect(ui->pushButton_doSomthing,&QPushButton::clicked
            ,this,&Widget::onButtonQThreadDoSomthingClicked);
    //心跳的關聯
    connect(&m_heart,&QTimer::timeout,this,&Widget::heartTimeOut);
    m_heart.setInterval(100);
    //全域性執行緒的建立
    //全域性執行緒建立時可以把窗體指標作為父物件
    m_thread = new ThreadFromQThread(this);
    //關聯執行緒的訊號和槽
    connect(m_thread,&ThreadFromQThread::message
            ,this,&Widget::receiveMessage);//
    connect(m_thread,&ThreadFromQThread::progress
            ,this,&Widget::progress);
    connect(m_thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);
    //UI心跳開始
    m_heart.start();
}

由於是全域性存在的執行緒,因此在窗體建立時就建立執行緒,可以把執行緒的父物件設定為窗體,這時需要注意,別手動delete執行緒指標。用於你的QThread是在Qt的事件迴圈裡面,手動delete會發生不可預料的意外。理論上所有QObject都不應該手動delete,如果沒有多執行緒,手動delete可能不會發生問題,但是多執行緒情況下delete非常容易出問題,那是因為有可能你要刪除的這個物件在Qt的事件迴圈裡還排隊,但你卻已經在外面刪除了它,這樣程式會發生崩潰。

如果你確實要刪除,請參閱void QObject::deleteLater () [slot]這個槽,這個槽非常有用,尤其是對區域性執行緒來說。後面會經常用到它用於安全的結束執行緒。

在需要啟動執行緒的地方呼叫start函式即可啟動執行緒。

 
void Widget::onButtonQThreadClicked()
{
    ui->progressBar->setValue(0);
    if(m_thread->isRunning())
    {
        return;
    }
    m_thread->start();
}

如果執行緒已經執行,你重複呼叫start其實是不會進行任何處理。

一個全域性執行緒就那麼簡單,要用的時候start一下就行。真正要注意的是如何在ui結束時把執行緒安全退出。

在widget的解構函式應該這樣寫:

 
Widget::~Widget()
{
    qDebug() << "start destroy widget";
    m_thread->stopImmediately();
    m_thread->wait();
    delete ui;
    qDebug() << "end destroy widget";
}

這裡要注意的是m_thread->wait();這一句,這一句是主執行緒等待子執行緒結束才能繼續往下執行,這樣能確保過程是單一往下進行的,也就是不會說子執行緒還沒結束完,主執行緒就destrioy掉了(m_thread的父類是主執行緒視窗,主執行緒視窗如果沒等子執行緒結束就destroy的話,會順手把m_threaddelete這時就會奔潰了),因此wait的作用就是掛起,一直等到子執行緒結束。

還有一種方法是讓QThread自己刪除自己,就是在new執行緒時,不指定父物件,通過繫結**void QObject::deleteLater () [slot]**槽讓它自動釋放。這樣在widget析構時可以免去m_thread->wait();這句。

2.4.2 如何啟動一個區域性執行緒(用完即釋放的執行緒)

啟動一個區域性執行緒(就是執行完自動刪除的執行緒)方法和啟動全域性執行緒差不多,但要關聯多一個槽函式,就是之前提到的**void QObject::deleteLater () [slot]**,這個槽函式是能安全釋放執行緒資源的關鍵(直接delete thread指標不安全)。

簡單的例子如下:

 
void Widget::onButtonQThreadRunLoaclClicked()
{
    //區域性執行緒的建立的建立
    ThreadFromQThread* thread = new ThreadFromQThread(NULL);//這裡父物件指定為NULL
    connect(thread,&ThreadFromQThread::message
            ,this,&Widget::receiveMessage);
    connect(thread,&ThreadFromQThread::progress
            ,this,&Widget::progress);
    connect(thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);
    connect(thread,&QThread::finished
            ,thread,&QObject::deleteLater);//執行緒結束後呼叫deleteLater來銷燬分配的記憶體
    thread->start();
}

這個例子還是啟動之前的執行緒,但不同的是:

  • new ThreadFromQThread(NULL);並沒有給他指定父物件
  • connect(thread,&QThread::finished ,thread,&QObject::deleteLater);執行緒結束後呼叫deleteLater來銷燬分配的記憶體。 再執行緒執行完成,發射finished訊號後會呼叫deleteLater函式,在確認訊息迴圈中沒有這個執行緒的物件後會銷燬。

但是要注意避免重複點按鈕重複呼叫執行緒的情況,對於一些需求,執行緒開啟後再點選按鈕不會再重新生成執行緒,一直等到當前執行緒執行完才能再次點選按鈕,這種情況很好處理,加個標記就可以實現,也一般比較少用。

另外更多見的需求是,再次點選按鈕,需要終結上次未執行完的執行緒,重新執行一個新執行緒。這種情況非常多見,例如一個普通的圖片瀏覽器,都會有下一張圖和上一張圖這種按鈕,瀏覽器載入圖片一般都線上程裡執行(否則點選超大圖片時圖片瀏覽器會類似卡死的狀態),使用者點選下一張圖片時需要終止正在載入的當前圖片,載入下一張圖片。你不能要求客戶要當前圖片載入完才能載入下一張圖片,這就幾乎淪為單執行緒了。這時候,就需要終止當前執行緒,開闢新執行緒載入下一個圖片。

這時,上面的函式將會是大概這個樣子的

UI的標頭檔案需要一個成員變數記錄正在執行的執行緒

 
private slots:
   void onLocalThreadDestroy(QObject* obj);
private:
   QThread* m_currentRunLoaclThread;

執行生成臨時執行緒的函式將變為

 
void Widget::onButtonQThreadRunLoaclClicked()
{
    //區域性執行緒的建立的建立
    if(m_currentRunLoaclThread)
    {
         m_currentRunLoaclThread->stopImmediately();
    }
    ThreadFromQThread* thread = new ThreadFromQThread(NULL);
    connect(thread,&ThreadFromQThread::message
            ,this,&Widget::receiveMessage);
    connect(thread,&ThreadFromQThread::progress
            ,this,&Widget::progress);
    connect(thread,&QThread::finished
            ,this,&Widget::onQThreadFinished);
    connect(thread,&QThread::finished
            ,thread,&QObject::deleteLater);//執行緒結束後呼叫deleteLater來銷燬分配的記憶體
    connect(thread,&QObject::destroyed,this,&Widget::onLocalThreadDestroy);
    thread->start();
    m_currentRunLoaclThread = thread;
}
 
void Widget::onLocalThreadDestroy(QObject *obj)
{
    if(qobject_cast<QObject*>(m_currentRunLoaclThread) == obj)
    {
        m_currentRunLoaclThread = NULL;
    }
}

這裡用一個臨時變數記錄當前正在執行的區域性執行緒,由於執行緒結束時會銷燬自己,因此要通知主執行緒把這個儲存執行緒指標的臨時變數設定為NULL 因此用到了QObject::destroyed訊號,線上程物件析構時通知UI把m_currentRunLoaclThread設定為nullptr;

2.5 繼承QThread的一些總結

  • QThread執行start函式之後,run函式還未執行完畢,再次start會出現什麼後果?

答案是:不會發生任何結果,QThread還是繼續執行它的run函式,run函式不會被重新呼叫。雖然線上程未結束時呼叫start不會出現什麼結果,但為了謹慎起見,還是建議在start之前進行判斷:

void Widget::onButtonQThreadClicked()
{
    ui->progressBar->setValue(0);
    if(m_thread->isRunning())
    {
        return;
    }
    m_thread->start();
}

這種呼叫方法估計瞭解過QThread的都知道

  • 線上程執行過程呼叫quit函式有什麼效果

答案是:不會發生任何效果,QThread不會因為你呼叫quit函式而退出正在執行到一半的run,正確退出執行緒的方法上面有介紹。那quit到底有什麼用的呢,這要到下篇才能看出它的作用。使用moveToThread方法執行多執行緒時,這個函式將有大作用。

  • 程式在退出時要判斷各執行緒是否已經退出,沒退出的應該讓它終止 如果不進行判斷,很可能程式退出時會崩潰。如果執行緒的父物件是視窗物件,那麼在窗體的解構函式中,還需要呼叫wait函式等待執行緒完全結束再進行下面的析構。

  • 善用QObject::deleteLater 和 QObject::destroyed來進行記憶體管理 由於多執行緒環境你不可預料下一步是哪個語句執行,因此,加鎖和自動刪除是很有用的工具,加鎖是通過效率換取安全,用Qt的訊號槽系統可以更有效的處理這些問題。

示例代:

--> 見 github

#Qt使用多執行緒的一些心得——2.繼承QObject的多執行緒使用方法

現在Qt官方並不是很推薦繼承QThread來實現多執行緒方法,而是極力推崇繼承QObject的方法來實現,當然用哪個方法實現要視情況而定,別弄錯了就行,估計Qt如此推崇繼承QObject的方法可能是QThread太容易用錯的原因。

#前言

上一篇介紹了傳統的多執行緒使用方法——繼承QThread來實現多執行緒,這也是很多框架的做法(MFC),但Qt還有一種多執行緒的實現方法,比直接繼承QThread更為靈活,就是直接繼承QObject實現多執行緒。

QObject是Qt框架的基本類,但凡涉及到訊號槽有關的類都是繼承於QObjectQObject是一個功能異常強大的類,它提供了Qt關鍵技術訊號和槽的支援以及事件系統的支援,同時它提供了執行緒操作的介面,也就是QObject是可以選擇不同的執行緒裡執行的。

QObject的執行緒轉移函式是:void moveToThread(QThread * targetThread) ,通過此函式可以把一個**頂層Object(就是沒有父級)**轉移到一個新的執行緒裡。

QThread非常容易被新手誤用,主要是QThread自身並不生存在它run函式所在的執行緒,而是生存在舊的執行緒中,此問題在上一篇重點描述了。由於QThread的這個特性,導致在呼叫QThread的非run函式容易在舊執行緒中執行,因此人們發現了一個新的魔改QThread的方法: 人們發現,咦,QThread也繼承QObjectQObject有個函式void moveToThread(QThread * targetThread)可以把Object的執行執行緒轉移,那麼:(下面是非常不推薦的魔改做法,別用此方法):

 
class MyThread : public QThread{
public:
    MyThread ()
   {
        moveToThread(this);
   }
……
};

直接把MyThread整個轉移到MyThread的新執行緒中,MyThread不僅run,其它函式也在新執行緒裡了。這樣的確可以執行正常,但這並不是QThread設計的初衷,Qt還專門發過一篇文章來吐槽這個做法。

在Qt4.8之後,Qt多執行緒的寫法最好還是通過QObject來實現,和執行緒的互動通過訊號和槽(實際上其實是通過事件)聯絡。

#繼承QObject的多執行緒實現

QObject來實現多執行緒有個非常好的優點,就是預設就支援事件迴圈(Qt的許多非GUI類也需要事件迴圈支援,如QTimerQTcpSocket),QThread要支援事件迴圈需要在QThread::run()中呼叫QThread::exec()來提供對訊息迴圈的支援,否則那些需要事件迴圈支援的類都不能正常傳送訊號,因此如果要使用訊號和槽,那就直接使用QObject來實現多執行緒。

看看Qt官方文件的例子:

 
class Worker : public QObject
{
    Q_OBJECT
 
public slots:
    void doWork(const QString &parameter) {
        QString result;
        /* ... here is the expensive or blocking operation ... */
        emit resultReady(result);
    }
 
signals:
    void resultReady(const QString &result);
};
 
class Controller : public QObject
{
    Q_OBJECT
    QThread workerThread;
public:
    Controller() {
        Worker *worker = new Worker;
        worker->moveToThread(&workerThread);
        connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
        connect(this, &Controller::operate, worker, &Worker::doWork);
        connect(worker, &Worker::resultReady, this, &Controller::handleResults);
        workerThread.start();
    }
    ~Controller() {
        workerThread.quit();
        workerThread.wait();
    }
public slots:
    void handleResults(const QString &);
signals:
    void operate(const QString &);
};

使用QObject建立多執行緒的方法如下:

  • 寫一個繼承QObject的類,對需要進行復雜耗時邏輯的入口函式宣告為槽函式
  • 此類在舊執行緒new出來,不能給它設定任何父物件
  • 同時宣告一個QThread物件,在官方例子裡,QThread並沒有new出來,這樣在析構時就需要呼叫QThread::wait(),如果是堆分配的話, 可以通過deleteLater來讓執行緒自殺
  • 把obj通過moveToThread方法轉移到新執行緒中,此時object已經是線上程中了
  • 把執行緒的finished訊號和object的deleteLater槽連線,這個訊號槽必須連線,否則會記憶體洩漏
  • 正常連線其他訊號和槽(在連線訊號槽之前呼叫moveToThread,不需要處理connect的第五個引數,否則就顯示宣告用Qt::QueuedConnection來連線)
  • 初始化完後呼叫'QThread::start()'來啟動執行緒
  • 在邏輯結束後,呼叫QThread::quit退出執行緒的事件迴圈

使用QObject來實現多執行緒比用繼承QThread的方法更加靈活,整個類都是在新的執行緒中,通過訊號槽和主執行緒傳遞資料,前篇文章的例子用繼承QObject的方法實現的話,程式碼如下: 標頭檔案(ThreadObject.h):

 
#include <QObject>
#include <QMutex>
class ThreadObject : public QObject
{
    Q_OBJECT
public:
    ThreadObject(QObject* parent = NULL);
    ~ThreadObject();
    void setRunCount(int count);
    void stop();
signals:
    void message(const QString& info);
    void progress(int present);
public slots:
    void runSomeBigWork1();
    void runSomeBigWork2();
private:
    int m_runCount;
    int m_runCount2;
    bool m_isStop;
 
    QMutex m_stopMutex;
};

cpp檔案(ThreadObject.cpp):

#include "ThreadObject.h"
#include <QThread>
#include <QDebug>
#include <QMutexLocker>
#include <QElapsedTimer>
#include <limits>
ThreadObject::ThreadObject(QObject *parent):QObject(parent)
  ,m_runCount(10)
  ,m_runCount2(std::numeric_limits<int>::max())
  ,m_isStop(true)
{
 
}
 
ThreadObject::~ThreadObject()
{
    qDebug() << "ThreadObject destroy";
    emit message(QString("Destroy %1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
}
 
void ThreadObject::setRunCount(int count)
{
    m_runCount = count;
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
}
 
void ThreadObject::runSomeBigWork1()
{
    {
        QMutexLocker locker(&m_stopMutex);
        m_isStop = false;
    }
    int count = 0;
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
    emit message(str);
    int process = 0;
    while(1)
    {
        {
            QMutexLocker locker(&m_stopMutex);
            if(m_isStop)
                return;
        }
        if(m_runCount == count)
        {
            break;
        }
        sleep(1);
        int pro = ((float)count / m_runCount) * 100;
        if(pro != process)
        {
            process = pro;
            emit progress(((float)count / m_runCount) * 100);
            emit message(QString("Object::run times:%1,m_runCount:%2").arg(count).arg(m_runCount2));
        }
        ++count;
    }
}
 
void ThreadObject::runSomeBigWork2()
{
    {
        QMutexLocker locker(&m_stopMutex);
        m_isStop = false;
    }
    int count = 0;
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
    emit message(str);
    int process = 0;
    QElapsedTimer timer;
    timer.start();
    while(1)
    {
        {
            QMutexLocker locker(&m_stopMutex);
            if(m_isStop)
                return;
        }
        if(m_runCount2 == count)
        {
            break;
        }
        int pro = ((float)count / m_runCount2) * 100;
        if(pro != process)
        {
            process = pro;
            emit progress(pro);
            emit message(QString("%1,%2,%3,%4")
                         .arg(count)
                         .arg(m_runCount2)
                         .arg(pro)
                         .arg(timer.elapsed()));
            timer.restart();
        }
        ++count;
    }
}
 
void ThreadObject::stop()
{
    QMutexLocker locker(&m_stopMutex);
    emit message(QString("%1->%2,thread id:%3").arg(__FUNCTION__).arg(__FILE__).arg((int)QThread::currentThreadId()));
    m_isStop = true;
}

這個Object有兩個耗時函式work1和work2,這兩個耗時函式的呼叫都是通過槽函式觸發,同時為了能及時打斷執行緒,添加了一個stop函式,stop函式不是通過訊號槽觸發,因此需要對資料進行保護,這裡用了互斥鎖對一個bool變數進行了保護處理,當然會失去一些效能。

主介面的標頭檔案(擷取部分程式碼):

 
#include <QWidget>
#include <QTimer>
class ThreadFromQThread;
class ThreadObject;
namespace Ui {
class Widget;
}
 
class Widget : public QWidget
{
    Q_OBJECT
 
public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();
signals:
    void startObjThreadWork1();
    void startObjThreadWork2();
private slots:
……
    void onButtonObjectMove2ThreadClicked();
    void onButtonObjectMove2Thread2Clicked();
    void onButtonObjectQuitClicked();
    void onButtonObjectThreadStopClicked();
    
 
 
    void progress(int val);
    void receiveMessage(const QString& str);
    void heartTimeOut();
 
private:
    void startObjThread(); 
private:
    Ui::Widget *ui;
    ……
    ThreadObject* m_obj;
    QThread* m_objThread;
 
};

cpp檔案

Widget::~Widget()
{
    qDebug() << "start destroy widget";
   
    if(m_objThread)
    {
        m_objThread->quit();
    }
    m_objThread->wait();
    qDebug() << "end destroy widget";
}
 
//建立執行緒
void Widget::startObjThread()
{
    if(m_objThread)
    {
        return;
    }
    m_objThread= new QThread();
    m_obj = new ThreadObject();
    m_obj->moveToThread(m_objThread);
    connect(m_objThread,&QThread::finished,m_objThread,&QObject::deleteLater);
    connect(m_objThread,&QThread::finished,m_obj,&QObject::deleteLater);
    connect(this,&Widget::startObjThreadWork1,m_obj,&ThreadObject::runSomeBigWork1);
    connect(this,&Widget::startObjThreadWork2,m_obj,&ThreadObject::runSomeBigWork2);
    connect(m_obj,&ThreadObject::progress,this,&Widget::progress);
    connect(m_obj,&ThreadObject::message,this,&Widget::receiveMessage);
 
    m_objThread->start();
}
 
//呼叫執行緒的runSomeBigWork1
void Widget::onButtonObjectMove2ThreadClicked()
{
    if(!m_objThread)
    {
        startObjThread();
    }
 
    emit startObjThreadWork1();//主執行緒通過訊號換起子執行緒的槽函式
 
    ui->textBrowser->append("start Obj Thread work 1");
}
//呼叫執行緒的runSomeBigWork2
void Widget::onButtonObjectMove2Thread2Clicked()
{
    if(!m_objThread)
    {
        startObjThread();
    }
    emit startObjThreadWork2();//主執行緒通過訊號換起子執行緒的槽函式
 
    ui->textBrowser->append("start Obj Thread work 2");
}
 
//呼叫執行緒的中斷
void Widget::onButtonObjectThreadStopClicked()
{
    if(m_objThread)
    {
        if(m_obj)
        {
            m_obj->stop();
        }
    }
}

建立執行緒和官方例子差不多,區別是QThread也是用堆分配,這樣,讓QThread自殺的槽就一定記得加上,否則QThread就逍遙法外了。

connect(m_objThread,&QThread::finished,m_objThread,&QObject::deleteLater);

#加了鎖對效能有多大的影響 上例的runSomeBigWork2中,讓一個int不停自加1,一直加到int的最大值,為了驗證加鎖和不加鎖的影響,這裡對加鎖和不加鎖運行了兩次觀察耗時的變化

 
void ThreadObject::runSomeBigWork2()
{
    {
        QMutexLocker locker(&m_stopMutex);
        m_isStop = false;
    }
    int count = 0;
    QString str = QString("%1->%2,thread id:%3").arg(__FILE__).arg(__FUNCTION__).arg((int)QThread::currentThreadId());
    emit message(str);
    int process = 0;
    QElapsedTimer timer;
    timer.start();
    while(1)
    {
        {
            QMutexLocker locker(&m_stopMutex);
            if(m_isStop)
                return;
        }
        if(m_runCount2 == count)
        {
            break;
        }
        int pro = ((float)count / m_runCount2) * 100;
        if(pro != process)
        {
            process = pro;
            emit progress(pro);
            emit message(QString("%1,%2,%3,%4")
                         .arg(count)
                         .arg(m_runCount2)
                         .arg(pro)
                         .arg(timer.elapsed()));
            timer.restart();
        }
        ++count;
    }
}

結果如下:

這裡沒個橫座標的每%1進行了21474837次迴圈,由統計圖可見,Debug模式下使用了鎖後效能下降4倍,Release模式下下降1.5倍的樣子