Qt中的多執行緒與執行緒池淺析+實例
1. Qt中的多執行緒與執行緒池
今天學習了Qt中的多執行緒和執行緒池,特寫這篇部落格來記錄一下
2. 多執行緒
2.1 執行緒類 QThread
Qt 中提供了一個執行緒類,通過這個類就可以創建子執行緒了,Qt 中一共提供了兩種創建子執行緒的方式,先看一下這個類中提供的一些常用 API 函數:
2.1.1 常用函數
// QThread 類常用 API
// 構造函數
QThread::QThread(QObject *parent = Q_NULLPTR);
// 判斷執行緒中的任務是不是處理完畢了
bool QThread::isFinished() const;
// 判斷子執行緒是不是在執行任務
bool QThread::isRunning() const;
// Qt中的執行緒可以設置優先順序
// 得到當前執行緒的優先順序
Priority QThread::priority() const;
void QThread::setPriority(Priority priority);
優先順序:
QThread::IdlePriority --> 最低的優先順序
QThread::LowestPriority
QThread::LowPriority
QThread::NormalPriority
QThread::HighPriority
QThread::HighestPriority
QThread::TimeCriticalPriority
QThread::InheritPriority --> 最高的優先順序, 默認是這個
// 退出執行緒, 停止底層的事件循環
// 退出執行緒的工作函數
void QThread::exit(int returnCode = 0);
// 調用執行緒退出函數之後, 執行緒不會馬上退出因為當前任務有可能還沒有完成, 調回用這個函數是
// 等待任務完成, 然後退出執行緒, 一般情況下會在 exit() 後邊調用這個函數
bool QThread::wait(unsigned long time = ULONG_MAX);
2.1.2 訊號槽
// 和調用 exit() 效果是一樣的
// 代用這個函數之後, 再調用 wait() 函數
[slot] void QThread::quit();
// 啟動子執行緒
[slot] void QThread::start(Priority priority = InheritPriority);
// 執行緒退出, 可能是會馬上終止執行緒, 一般情況下不使用這個函數
[slot] void QThread::terminate();
// 執行緒中執行的任務完成了, 發出該訊號
// 任務函數中的處理邏輯執行完畢了
[signal] void QThread::finished();
// 開始工作之前發出這個訊號, 一般不使用
[signal] void QThread::started();
2.1.3 靜態函數
// 返回一個指向管理當前執行執行緒的QThread的指針
[static] QThread *QThread::currentThread();
// 返回可以在系統上運行的理想執行緒數 == 和當前電腦的 CPU 核心數相同
[static] int QThread::idealThreadCount();
// 執行緒休眠函數
[static] void QThread::msleep(unsigned long msecs); // 單位: 毫秒
[static] void QThread::sleep(unsigned long secs); // 單位: 秒
[static] void QThread::usleep(unsigned long usecs); // 單位: 微秒
2.1.4 run()函數
// 子執行緒要處理什麼任務, 需要寫到 run() 中
[virtual protected] void QThread::run();
run()函數非常重要,當執行緒執行的時候,就是去執行run()函數中的程式碼
2.2 使用方式一
- 需要創建一個執行緒類的子類,讓其繼承 QT 中的執行緒類 QThread
- 重寫父類的 run () 方法,在該函數內部編寫子執行緒要處理的具體的業務流程
- 在主執行緒中創建子執行緒對象,new 一個就可以了
- 啟動子執行緒,調用 start () 方法
當子執行緒別創建出來之後,父子執行緒之間的通訊可以通過訊號槽的方式,注意事項:
- 在 Qt 中在子執行緒中不要操作程式中的窗口類型對象,不允許,如果操作了程式就掛了
- 只有主執行緒才能操作程式中的窗口對象,默認的執行緒就是主執行緒,自己創建的就是子執行緒
2.3 實例
現在我們來完成一個功能,就是先隨機生成很多隨機數,然後通過冒泡排序,和快速排序的方法去執行,並且顯示出來
-
首先畫出一個窗口
長這個樣子 -
創建執行緒類 MyThread
說明:
- Generate類是用來生成隨機數的,其中有個槽方法recvNum,用來接受start訊號傳進來的參數,參數的值為生成的隨機數的個數,run()方法為生成隨機數的程式碼
- BubbleSort和QuickSort類 都是用來排序的類,只是排序的方法不同,其中recvArray是為了接受傳過來的隨機數用來排序,finish訊號是將排序好的數組傳給主執行緒
頭文件 MyThread.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>
#include <QVector>
class Generate : public QThread
{
Q_OBJECT
public:
explicit Generate(QObject *parent = nullptr);
void recvNum(int num);
protected:
void run() override;
private:
int m_num;
signals:
void sendArray(QVector<int> num);
};
// 冒泡執行緒類
class BubbleSort : public QThread
{
Q_OBJECT
public:
explicit BubbleSort(QObject *parent = nullptr);
void recvArray(QVector<int> list);
protected:
void run() override;
private:
QVector<int> m_list;
signals:
void finish(QVector<int> num);
};
// 快排執行緒類
class QuickSort : public QThread
{
Q_OBJECT
public:
explicit QuickSort(QObject *parent = nullptr);
void recvArray(QVector<int> list);
protected:
void run() override;
private:
QVector<int> m_list;
void quickSort(QVector<int> &s,int l,int r);
signals:
void finish(QVector<int> num);
};
#endif // MYTHREAD_H
源文件mythread.cpp
說明:
- 這是對上述一些函數的實現
#include "mythread.h"
#include <QDebug>
#include <QElapsedTimer>
Generate::Generate(QObject *parent) : QThread(parent)
{
}
void Generate::recvNum(int num)
{
m_num = num;
}
void Generate::run()
{
qDebug() << "生成隨機數的執行緒地址: " << QThread::currentThread();
QVector<int> list;
QElapsedTimer time;
time.start();
for(int i=0;i<m_num;i++)
{
list.push_back(qrand()%10000);
}
int milsec = time.elapsed();
qDebug() << "生成" << m_num << "個隨機數總共用時: " << milsec << "毫秒";
emit sendArray(list);
}
BubbleSort::BubbleSort(QObject *parent) : QThread(parent)
{
}
void BubbleSort::recvArray(QVector<int> list)
{
m_list = list;
}
void BubbleSort::run()
{
qDebug() << "冒泡排序的執行緒地址: " << QThread::currentThread();
QElapsedTimer time;
time.start();
for(int i=0;i<m_list.size();i++){
for(int j=0;j<m_list.size()-i-1;j++){
if(m_list[j]>m_list[j+1]){
int temp = m_list[j];
m_list[j] = m_list[j+1];
m_list[j+1] = temp;
}
}
}
int milsec = time.elapsed();
qDebug() << "冒泡排序用時: " << milsec << "毫秒";
emit finish(m_list);
}
QuickSort::QuickSort(QObject *parent) : QThread(parent)
{
}
void QuickSort::recvArray(QVector<int> list)
{
m_list = list;
}
void QuickSort::run()
{
qDebug() << "快速排序的執行緒地址: " << QThread::currentThread();
QElapsedTimer time;
time.start();
quickSort(m_list,0,m_list.size()-1);
int milsec = time.elapsed();
qDebug() << "快速排序用時: " << milsec << "毫秒";
emit finish(m_list);
}
void QuickSort::quickSort(QVector<int> &s, int l, int r)
{
if (l< r)
{
int i = l, j = r, x = s[l];
while (i < j)
{
while(i < j && s[j]>= x) // 從右向左找第一個小於x的數
j--;
if(i < j)
s[i++] = s[j];
while(i < j && s[i]< x) // 從左向右找第一個大於等於x的數
i++;
if(i < j)
s[j--] = s[i];
}
s[i] = x;
quickSort(s, l, i - 1); // 遞歸調用
quickSort(s, i + 1, r);
}
}
- 在主窗口類中實現相關功能
說明:
- 頭文件中定義了訊號函數start(int num)用來發出訊號,告訴要生成的隨機數的數量
頭文件MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
Ui::MainWindow *ui;
signals:
void starting(int num);
};
#endif // MAINWINDOW_H
源文件mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mythread.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 1. 創建子執行緒對象
Generate* gen = new Generate;
BubbleSort* bubble = new BubbleSort;
QuickSort* quick = new QuickSort;
connect(this,&MainWindow::starting,gen,&Generate::recvNum);
// 2. 啟動子執行緒
connect(ui->startBtn,&QPushButton::clicked,this,[=](){
emit starting(10000);
gen->start();
});
connect(gen,&Generate::sendArray,bubble,&BubbleSort::recvArray);
connect(gen,&Generate::sendArray,quick,&QuickSort::recvArray);
// 3. 接受子執行緒發送的數據
connect(gen,&Generate::sendArray,this,[=](QVector<int> list)
{
bubble->start();
quick->start();
for(int i=0; i<list.size();++i){
ui->randList->addItem(QString::number(list.at(i)));
}
});
connect(bubble,&BubbleSort::finish,this,[=](QVector<int> list)
{
for(int i=0; i<list.size();++i){
ui->bubbleList->addItem(QString::number(list.at(i)));
}
});
connect(quick,&QuickSort::finish,this,[=](QVector<int> list)
{
for(int i=0; i<list.size();++i){
ui->quickList->addItem(QString::number(list.at(i)));
}
});
connect(this,&MainWindow::destroyed,this,[=](){
gen->quit();
gen->wait();
gen->deleteLater(); // delete gen;
bubble->quit();
bubble->wait();
bubble->deleteLater();
quick->quit();
quick->wait();
quick->deleteLater();
});
}
MainWindow::~MainWindow()
{
delete ui;
}
注意最後對執行緒的析構
connect(this,&MainWindow::destroyed,this,[=](){
gen->quit();
gen->wait();
gen->deleteLater(); // delete gen;
bubble->quit();
bubble->wait();
bubble->deleteLater();
quick->quit();
quick->wait();
quick->deleteLater();
});
- 運行結果
可以發現,在對一萬個隨機數進行排序時,快速排序要比冒泡排序快很多
2.4 多執行緒使用方式二
Qt 提供的第二種執行緒的創建方式彌補了第一種方式的缺點,用起來更加靈活,但是這種方式寫起來會相對複雜一些,其具體操作步驟如下:
- 創建一個新的類,讓這個類從 QObject 派生
- 在這個類中添加一個公共的成員函數,函數體就是我們要子執行緒中執行的業務邏輯
- 在主執行緒中創建一個 QThread 對象,這就是子執行緒的對象
- 在主執行緒中創建工作的類對象(千萬不要指定給創建的對象指定父對象)
- 將 MyWork 對象移動到創建的子執行緒對象中,需要調用 QObject 類提供的 moveToThread() 方法
- 啟動子執行緒,調用 start(), 這時候執行緒啟動了,但是移動到執行緒中的對象並沒有工作
- 調用 MyWork 類對象的工作函數,讓這個函數開始執行,這時候是在移動到的那個子執行緒中運行的
使用這種多執行緒方式,假設有多個不相關的業務流程需要被處理,那麼就可以創建多個類似於 MyWork 的類,將業務流程放多類的公共成員函數中,然後將這個業務類的實例對象移動到對應的子執行緒中 moveToThread() 就可以了,這樣可以讓編寫的程式更加靈活,可讀性更強,更易於維護。
2.5 方式一與方式二區別
- 方式一需要重載run()函數,run()函數不能帶有參數,使得我們獲取參數只能通過訊號槽的方式去解決
- 方式二更加靈活,將一個方法可以放到同一個執行緒中,也可以放到不同的執行緒中
3. 執行緒池
3.1 執行緒池原理
我們使用執行緒的時候就去創建一個執行緒,這樣實現起來非常簡便,但是就會有一個問題:如果並發的執行緒數量很多,並且每個執行緒都是執行一個時間很短的任務就結束了,這樣頻繁創建執行緒就會大大降低系統的效率,因為頻繁創建執行緒和銷毀執行緒需要時間。
那麼有沒有一種辦法使得執行緒可以復用,就是執行完一個任務,並不被銷毀,而是可以繼續執行其他的任務呢?
執行緒池是一種多執行緒處理形式,處理過程中將任務添加到隊列,然後在創建執行緒後自動啟動這些任務。執行緒池執行緒都是後台執行緒。每個執行緒都使用默認的堆棧大小,以默認的優先順序運行,並處於多執行緒單元中。如果某個執行緒在託管程式碼中空閑(如正在等待某個事件), 則執行緒池將插入另一個輔助執行緒來使所有處理器保持繁忙。如果所有執行緒池執行緒都始終保持繁忙,但隊列中包含掛起的工作,則執行緒池將在一段時間後創建另一個輔助執行緒但執行緒的數目永遠不會超過最大值。超過最大值的執行緒可以排隊,但他們要等到其他執行緒完成後才啟動。
在各個程式語言的語種中都有執行緒池的概念,並且很多語言中直接提供了執行緒池,作為程式猿直接使用就可以了,下面給大家介紹一下執行緒池的實現原理:
執行緒池的組成主要分為 3 個部分,這三部分配合工作就可以得到一個完整的執行緒池:
-
任務隊列,存儲需要處理的任務,由工作的執行緒來處理這些任務
1)通過執行緒池提供的 API 函數,將一個待處理的任務添加到任務隊列,或者從任務隊列中刪除
2)已處理的任務會被從任務隊列中刪除
3)執行緒池的使用者,也就是調用執行緒池函數往任務隊列中添加任務的執行緒就是生產者執行緒 -
工作的執行緒(任務隊列任務的消費者) ,N 個
1) 執行緒池中維護了一定數量的工作執行緒,他們的作用是是不停的讀任務隊列,從裡邊取出任務並處理
2) 工作的執行緒相當於是任務隊列的消費者角色,
3) 如果任務隊列為空,工作的執行緒將會被阻塞 (使用條件變數 / 訊號量阻塞)
4) 如果阻塞之後有了新的任務,由生產者將阻塞解除,工作執行緒開始工作 -
管理者執行緒(不處理任務隊列中的任務),1 個
1) 它的任務是周期性的對任務隊列中的任務數量以及處於忙狀態的工作執行緒個數進行檢測
2) 當任務過多的時候,可以適當的創建一些新的工作執行緒
3) 當任務過少的時候,可以適當的銷毀一些工作的執行緒
3.2 QRunnable
在 Qt 中使用執行緒池需要先創建任務,添加到執行緒池中的每一個任務都需要是一個 QRunnable 類型,因此在程式中需要創建子類繼承 QRunnable 這個類,然後重寫 run() 方法,在這個函數中編寫要在執行緒池中執行的任務,並將這個子類對象傳遞給執行緒池,這樣任務就可以被執行緒池中的某個工作的執行緒處理掉了。
QRunnable 類 常用函數不多,主要是設置任務對象傳給執行緒池後,是否需要自動析構。
// 在子類中必須要重寫的函數, 裡邊是任務的處理流程
[pure virtual] void QRunnable::run();
// 參數設置為 true: 這個任務對象在執行緒池中的執行緒中處理完畢, 這個任務對象就會自動銷毀
// 參數設置為 false: 這個任務對象在執行緒池中的執行緒中處理完畢, 對象需要程式猿手動銷毀
void QRunnable::setAutoDelete(bool autoDelete);
// 獲取當然任務對象的析構方式,返回true->自動析構, 返回false->手動析構
bool QRunnable::autoDelete() const;
3.3 QThreadPool
Qt 中的 QThreadPool 類管理了一組 QThreads, 裡邊還維護了一個任務隊列。QThreadPool 管理和回收各個 QThread 對象,以幫助減少使用執行緒的程式中的執行緒創建成本。每個Qt應用程式都有一個全局 QThreadPool 對象,可以通過調用 globalInstance() 來訪問它。也可以單獨創建一個 QThreadPool 對象使用。
// 獲取和設置執行緒中的最大執行緒個數
int maxThreadCount() const;
void setMaxThreadCount(int maxThreadCount);
// 給執行緒池添加任務, 任務是一個 QRunnable 類型的對象
// 如果執行緒池中沒有空閑的執行緒了, 任務會放到任務隊列中, 等待執行緒處理
void QThreadPool::start(QRunnable * runnable, int priority = 0);
// 如果執行緒池中沒有空閑的執行緒了, 直接返回值, 任務添加失敗, 任務不會添加到任務隊列中
bool QThreadPool::tryStart(QRunnable * runnable);
// 執行緒池中被激活的執行緒的個數(正在工作的執行緒個數)
int QThreadPool::activeThreadCount() const;
// 嘗試性的將某一個任務從執行緒池的任務隊列中刪除, 如果任務已經開始執行就無法刪除了
bool QThreadPool::tryTake(QRunnable *runnable);
// 將執行緒池中的任務隊列裡邊沒有開始處理的所有任務刪除, 如果已經開始處理了就無法通過該函數刪除了
void QThreadPool::clear();
// 在每個Qt應用程式中都有一個全局的執行緒池對象, 通過這個函數直接訪問這個對象
static QThreadPool * QThreadPool::globalInstance();
一般情況下,我們不需要在 Qt 程式中創建執行緒池對象,直接使用 Qt 為每個應用程式提供的執行緒池全局對象即可。得到執行緒池對象之後,調用 start() 方法就可以將一個任務添加到執行緒池中,這個任務就可以被執行緒池內部的執行緒池處理掉了,使用執行緒池比自己創建執行緒的這種多種多執行緒方式更加簡單和易於維護。
3.4 實例
我們將之前的程式改為執行緒池來實現,具體的方法就是創建執行緒池,然後將這些執行緒添加到執行緒中
發現:
圖中生成隨機數的執行緒地址和冒泡排序的地址是相同的,這是為什麼呢?
- 因為當生成隨機數的執行緒執行完之後,沒有事情做了,這個時候它就會去處理下一個任務,因此地址相同
- 而且放入執行緒池當中的任務,不需要自己去釋放,而是執行緒池統一管理