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


1. 引言

多線程對於需要處理耗時任務的應用很有用,一方面響應用戶操作、更新界面顯示,另一方面在“后台”進行耗時操作,比如大量運算、復制大文件、網絡傳輸等。
使用Qt框架開發應用程序時,使用QThread類可以方便快捷地創建管理多線程。而多線程之間的通信也可使用Qt特有的“信號-槽”機制實現。
下面的說明以文件復制為例。主線程負責提供交互界面,顯示復制進度等;子線程負責復制文件。最后附有可以執行的代碼。

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

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

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

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

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

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:

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

// mainwindow.cpp中,初始化時
    m_childThread = new QThread; // 子線程,本身不負責復制

然后使用moveToThread()將m_copier移動到新線程。注意moveToThread()是QObject的公有函數,因此用於復制文件的類FileCopier必須繼承自QObject。移動之后啟動子線程。此時復制還沒有開始。

    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


免責聲明!

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



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