在Qt(C++)中使用QThread實現多線程

1. 引言

多線程對於須要處理耗時任務的應用頗有用,一方面響應用戶操做、更新界面顯示,另外一方面在「後臺」進行耗時操做,好比大量運算、複製大文件、網絡傳輸等。 使用Qt框架開發應用程序時,使用QThread類能夠方便快捷地建立管理多線程。而多線程之間的通訊也可以使用Qt特有的「信號-槽」機制實現。 下面的說明以文件複製爲例。主線程負責提供交互界面,顯示覆制進度等;子線程負責複製文件。最後附有能夠執行的代碼。c++

2. QThread使用方法1——重寫run()函數

第一種使用方法是本身寫一個類繼承QThread,並重寫其run()函數。 你們知道,C/C++程序都是從main()函數開始執行的。main()函數其實就是主進程的入口,main()函數退出了,則主進程退出,整個進程也就結束了。 而對於使用Qthread建立的進程而言,run()函數則是新線程的入口,run()函數退出,意味着線程的終止。複製文件的功能,就是在run()函數中執行的。 下面舉個文件複製的例子。自定義一個類,繼承自Qthreadgit

CopyFileThread: public QThread
{
    Q_OBJECT
public:
    CopyFileThread(QObject * parent = 0);

protected:
    void run(); // 新線程入口
// 省略掉一些內容
}

在對應的cpp文件中,定義run()github

void CopyFileThread::run()
{
    // 新線程入口
    // 初始化和操做放在這裏
}

將這個類寫好以後,在主線程的代碼中生成一個CopyFileThread的實例,例如在mainwindow.cpp中寫:網絡

// mainwindow.h中
CopyFileThread * m_cpyThread;

// mainwindow.cpp中
m_cpyThread = new CopyFileThread;

在要開始複製的時候,好比按下「複製」按鈕後,讓這個線程開始執行:多線程

m_cpyThread->start();

注意,使用start()函數來啓動子線程,而不是run()。start()會自動調用run()。 線程開始執行後,就進入run()函數,執行復制文件的操做。而此時,主線程的顯示和操做都不受影響。 若是須要進行對複製過程當中可能發生的事件進行處理,例如界面顯示覆制進度、出錯返回等等,應該從CopyFileThread中發出信號(signal),並事先鏈接到mainwindow的槽,由這些槽函數來處理事件。框架

3. QThread使用方法2——moveToThread()

若是不想每執行一種任務就自定義一個新線程,那麼能夠自定義用於完成任務的類,並讓它們繼承自QObject。例如,自定義一個FileCopier類,用於複製文件。函數

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

public slots:
    void startCopying();
    void cancelCopying();
}

注意這裏咱們定義了兩個槽函數,分別用於複製的開始和取消。 這個類自己的實例化是在主線程中進行的,例如:動畫

// mainwindow.h中
private:
    FileCopier* m_copier;

// mainwindow.cpp中,初始化時
    m_copier = new FileCopier;

此時m_copier仍是屬於主線程的。要將其移動到子線程處理,須要首先聲明並實例化一個QThread:this

// mainwindow.h中
signals:
    void startCopyRsquested();
private:
    QThread * m_childThread; // m_copier將被移動到此線程執行

// mainwindow.cpp中,初始化時
    m_childThread = new QThread; // 子線程,自己不負責複製

而後使用moveToThread()將m_copier移動到新線程。注意moveToThread()是QObject的公有函數,所以用於複製文件的類FileCopier必須繼承自QObject。移動以後啓動子線程。此時複製尚未開始。spa

m_copier->moveToThread(m_childThread); // 將實例移動到新的線程,實現多線程運行
    m_childThread->start(); // 啓動子線程

注意必定要記得啓動子線程,不然線程沒有運行,m_copier的功能也沒法執行。 要開始複製,須要使用信號-槽機制,觸發FileCopier的槽函數實現。所以要事先定義信號並鏈接:

// mainwindow.h中
signals:
    void startCopyRsquested();
// mainwindow.cpp中,初始化時
// 使用信號-槽機制,發出開始指令
    connect(this, SIGNAL(startCopyRsquested()), m_copier, SLOT(startCopying()));

當按下「複製」按鈕後,發出信號。

emit startCopyRsquested(); // 發送信號

m_copier在另外一個線程接收到信號後,觸發槽函數,開始複製文件。

4.常見問題

4.1. 子線程中能不能進行UI操做?

Qt中的UI操做,好比QMainWindow、QWidget之類的建立、操做,只能位於主線程! 這個限制意味着你不能在新的線程中使用QDialog、QMessageBox等。好比在新線程中複製文件出錯,想彈出對話框警告?能夠,可是必須將錯誤信息傳到主線程,由主線程實現對話框警告。 所以通常思路是,主線程負責提供界面,子線程負責無UI的單一任務,經過「信號-槽」與主線程交互。

4.2. QThread中的哪些代碼屬於子線程?

QThread,以及繼承QThread的類(如下統稱QThread),他們的實例都屬於新線程嗎?答案是:不。 須要注意的是,QThread自己的實例是屬於建立該實例的線程的。好比在主線程中建立一個QThread,那麼這個QThread實例自己屬於主線程。固然,QThread會開闢一個新線程(入口是run()),可是QThread自己並不屬於這個新線程。也就是說,QThread自己的成員都不屬於新線程,並且在QThread構造函數裏經過new獲得的實例,也不屬於新線程。這一特性意味着,若是要實現多線程操做,那麼你但願屬於新線程的實例、變量等,應該在run()中進行初始化、實例化等操做。本文給出的例子就是這樣操做的。 若是你的多線程程序運行起來,會出現關於thread的報警,思考一下,各類變量、實例是否是放對了位置,是否是真的位於新的線程裏。

4.3. 怎麼查看是否是真的實現了多線程?

能夠打印出當前線程。對於全部繼承自QObject的類,例如QMainwindow、QThread,以及自定義的各類類,能夠調用QObject::thread()查看當前線程,這個函數返回的是一個QThread的指針。例如用qDebug()打印: 在mainwindow.cpp的某個函數裏、QThread的run()函數裏、自定義類的某個函數裏,寫上:

qDebug() << "Current thread:" << thread();

對比不一樣位置打印的指針,就能夠知道它們是否是位於同一個線程了。

5.範例

範例實現了多線程複製文本文件。 提供的範例文件可用QtCreator編譯運行。界面以下(不一樣的操做系統略有不一樣):

範例中實現了本文介紹的兩種方法,同時也給出了單線程複製對比。打鉤選擇不一樣的複製方法。能夠發現,在使用多線程的時候,界面不會假死,第二根進度條的動畫是持續的;而使用單線程複製的時候,「取消」按鈕按不動,界面假死,並且第二根進度條的動畫也中止了。 因爲範例處理的文件很小,爲了讓複製過程持續較長時間以便使得現象明顯,複製文件的時候,每複製一行加入了等待。

範例代碼: https://github.com/Xia-Weiwen/CopyFile

相關文章
相關標籤/搜索