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