上篇文章中簡單介紹瞭如何使用 Windows API 和c++11中的 std::thread 創建線程。
線程的創建和基本使用
本篇文章將會介紹如何使用QThread創建線程。
- QThread是Qt所有線程控制的基礎,每一個QThread實例對象控制一個線程。
- QThread可以直接實例化使用也可以用繼承的方式使用,QThread以事件循環的方式,允許繼承自QObject的槽函數在線程中被調用執行。子類化QThread可以在開啓線程事件循環之前初始化一個新線程,或者不使用事件循環的方式執行並行代碼。
1. 使用信號和槽的形式觸發
QThread的入口執行函數是 run() 函數,默認 run() 函數會通過調用函數 exec() 開啓事件循環在線程中。可以使用函數 QObject::moveToThread() 將一個工作對象與線程對象相關聯。
下面是一個簡單的示例,示例中在一個新線程中計算前n個數的和後通過信號返回給調用者:
工作類頭文件, Worker.h
#ifndef WORKER_H
#define WORKER_H
#include <QObject>
class Worker : public QObject
{
Q_OBJECT
public:
Worker(QObject* parent = nullptr);
~Worker();
public slots:
// 計算前count個數的和
void doWork(int count);
signals:
// 發送計算完成信號
void doFinished(int);
};
#endif
工作類CPP文件, Worker.cpp
#include "Worker.h"
#include <QDebug>
#include <QThread>
Worker::Worker(QObject* parent)
:QObject(parent)
{
}
Worker::~Worker()
{
}
// 計算 0~count個數的和
void Worker::doWork(int count)
{
int sum = 0;
for (int i=0; i<=count; ++i)
sum += i;
// 打印當前函數名,線程ID,以及計算結果
qDebug() << __FUNCTION__ << "Thread ID: " << QThread::currentThreadId() << ", Result is " << sum;
emit doFinished(sum);
}
槽函數 void doWork(int count); 用來計算前count個數的和,計算完成後,發送信號 doFinished(int) 其中的參數是計算結果。這就是一個工作類,與線程一點關係沒有。
接下來是控制器
控制器頭文件,Controller.h
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include <QObject>
#include <QThread>
class Controller : public QObject
{
Q_OBJECT
public:
Controller(QObject* parent = nullptr);
~Controller();
// 開啓線程計算
void startThreadRunFunc(int number);
private:
QThread m_thread;
signals:
// 該信號用於觸發工作者中的槽函數
void startCalcSum(int);
private slots:
// 接受計算完畢後的結果槽函數
void onCalcSumFinished(int sum);
};
#endif
控制器CPP文件,Controller.cpp
#include "Controller.h"
#include "Worker.h"
#include <QDebug>
Controller::Controller(QObject* parent)
:QObject (parent)
{
// [1]
Worker* worker = new Worker;
worker->moveToThread(&m_thread);
// [2]
QObject::connect(this, &Controller::startCalcSum, worker, &Worker::doWork);
// [3]
QObject::connect(worker, &Worker::doFinished, this, &Controller::onCalcSumFinished);
// [4] 當線程退出時,釋放工作者內存
QObject::connect(&m_thread, &QThread::finished, worker, &Worker::deleteLater);
// [5]
m_thread.start();
}
Controller::~Controller()
{
m_thread.quit();
m_thread.wait();
}
void Controller::startThreadRunFunc(int number)
{
// 發送開始計算信號
emit startCalcSum(number);
qDebug() << __FUNCTION__ << " : Current Thread is " << QThread::currentThreadId();
}
void Controller::onCalcSumFinished(int sum)
{
// 打印行數名,當前線程ID,計算結果
qDebug() << __FUNCTION__ \
<< " : Current Thread is " << QThread::currentThreadId() \
<< ", Result is " << sum;
}
構造函數中,主要做了如下步驟:
- 首先創建工作者對象,並與線程相關聯。
- 連接控制器的 startCalcSum 信號和工作者的 doWork 槽函數,即發送 startCalcSum 信號時,觸發 doWork 槽函數。這裏要說明的是,因爲是跨線程的信號和槽的鏈接,這裏默認的鏈接方式是使用 隊列連接 。具體信號和槽的鏈接方式可參考 Qt中的信號和槽 。
- 連接工作者的 doFinished 信號和控制器的 onCalcSumFinished 函數。當計算完成時,會觸發 doFinished 信號,同樣這裏也是 隊列連接 的方式。
- 連接線程的 finished 信號和工作者的 deleteLater 槽函數。當線程中不再有事件被執行並且事件循環停止退出的時候,QThread發送該 finished 信號。
- 調用函數 start() 開啓線程。
main函數中代碼如下:
#include <QCoreApplication>
#include <QThread>
#include "Controller.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 創建控制器
Controller *object = new Controller;
// 計算前100個數的和
object->startThreadRunFunc(100);
return a.exec();
}
執行結果如下:
Controller::startThreadRunFunc : Current Thread is 0x491c
Worker::doWork Thread ID: 0x62c0 , Result is 5050
Controller::onCalcSumFinished : Current Thread is 0x491c , Result is 5050
整體流程如下:
- 函數 startThreadRunFunc 發送信號 startCalcSum ,此過程在主線程中執行。
- 觸發 doWork 槽函數,計算前100個數的和,併發送信號 doFinished 信號,此過程在新建的線程中執行。
- 觸發 onCalcSumFinished 槽函數,此過程在主線程中執行。
2. 使用繼承自QThread方式觸發
在Qt4.x的時候,QThread的常用方式是繼承QThread重載函數 run() 。run() 函數是新線程的入口函數。我們同樣完成上功能,代碼如下:
CThread頭文件:
#ifndef CTHREAD_H
#define CTHREAD_H
#include <QThread>
#include <atomic>
class CThread : public QThread
{
Q_OBJECT
public:
CThread(QObject* parent = nullptr);
~CThread();
// 線程入口函數
void run(void) override;
// 計算前 0 ~ number的和
void calcSum(int number);
private:
std::atomic<bool> m_startThread;
std::atomic<int> m_number;
signals:
// 發送計算完成信號
void doFinished(int);
private slots:
// 相應計算完成結果
void onDoFinished(int sum);
};
#endif
CThread源文件
#include "CThread.h"
#include <QDebug>
CThread::CThread(QObject* parent)
:QThread (parent)
,m_startThread(false)
,m_number(0)
{
QObject::connect(this, &CThread::doFinished, this, &CThread::onDoFinished);
this->start();
}
CThread::~CThread()
{
this->requestInterruption();
this->wait();
}
void CThread::run(void)
{
while (!this->isInterruptionRequested())
{
// 判斷是否開啓線程計算
if (!m_startThread)
{
QThread::msleep(20);
continue;
}
// 計算 0 ~ m_number的和
int number = m_number;
int sum = 0;
for (int i = 0; i<=number; ++i)
sum += i;
// 打印函數名,線程ID,結果
qDebug() << __FUNCTION__ \
<< " : Current Thread Id is " << QThread::currentThreadId() \
<< ", Result is " << sum;
m_startThread = false;
// 發送信號
emit doFinished(sum);
}
}
// 計算前 0 ~ number的和
void CThread::calcSum(int number)
{
m_number = number;
m_startThread = true;
}
void CThread::onDoFinished(int sum)
{
// 打印函數名,線程ID,結果
qDebug() << __FUNCTION__ \
<< " : Current Thread Id is " << QThread::currentThreadId() \
<< ", Result is " << sum;
}
在 run() 函數,循環執行
- 函數 isInterruptionRequested() 默認值爲false,放調用函數 requestInterruption() 函數時,isInterruptionRequested() 的返回值爲true,且這兩個函數都是線程安全的。
- 通過變量 m_startThread 判斷是否需要執行計算,這是一個 std::atomic<bool> 類型的變量,爲原子量,爲了保證共享內容的線程安全。
- 計算併發送信號 doFinished()
調用部分如下:
#include <QCoreApplication>
#include "CThread.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// 使用繼承QThread的方式開啓線程計算前100個數的和
CThread *thread = new CThread;
thread->calcSum(100);
return a.exec();
}
結果如下:
CThread::run : Current Thread Id is 0x68c0 , Result is 5050
CThread::onDoFinished : Current Thread Id is 0x5ce0 , Result is 5050
分析:
- 調用函數 calcSum 函數,在新線程中計算前100個數的和併發送信號 doFinished 。
- 主線程接收信號,並執行槽函數 onDoFinished 。
3. 幾點說明
關於QThread我個人認知的一點點說明:
(1)使用信號和槽的方式是Qt的推薦方式,有兩點好處:
- 可以分離線程和具體實現,比如工作者對象可以在單獨的線程中執行,也可以在主線程中執行。
- 可以有多個線程的函數入口,創建多少個槽函數就有多少個線程函數入口。
(2)關於線程的等待退出
- 信號和槽的方式,使用如下代碼:
m_thread.quit();
m_thread.wait();
quit() 函數會退出事件循環,wait() 函數阻塞等待線程退出。
- 繼承QThread的方式,使用如下代碼:
this->requestInterruption();
this->wait();
當使用 isInterruptionRequested() 在 run() 函數作爲循環條件時,可以先請求退出,然後再阻塞等待線程的退出。
(3) 線程對象和線程是兩個不同的概念。比如上面的例子
CThread *thread = new CThread;
thread 對象就是一個線程對象,該對象的歸屬是主線程。因此該線程對象的槽函數的執行是在主線程中的;使用函數 moveToThread() 是更改對象的歸屬線程,因此信號和槽的方式觸發函數的執行是在新線程中。值得注意的是,線程中實現槽函數的觸發,必須需要執行事件循環即 exec() 函數。
(4)GUI的相關操作只能在主線程中完成。
QWidget等對象的創建和操作必須在主線程中完成,其他的非界面相關的類可以在不同的線程中操作。 moveToThread() 的對象及其父對象必須在同一個線程中。
作者:douzhq
個人主頁:http://www.douzhq.cn
文章同步頁(文章末尾可下載代碼): http://www.douzhq.cn/thread_qthread/