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 例項
我們將之前的程式改為執行緒池來實現,具體的方法就是建立執行緒池,然後將這些執行緒新增到執行緒中
發現:
圖中生成隨機數的執行緒地址和氣泡排序的地址是相同的,這是為什麼呢?
- 因為當生成隨機數的執行緒執行完之後,沒有事情做了,這個時候它就會去處理下一個任務,因此地址相同
- 而且放入執行緒池當中的任務,不需要自己去釋放,而是執行緒池統一管理