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 使用方式一

  1. 需要創建一個線程類的子類,讓其繼承 QT 中的線程類 QThread
  2. 重寫父類的 run () 方法,在該函數內部編寫子線程要處理的具體的業務流程
  3. 在主線程中創建子線程對象,new 一個就可以了
  4. 啟動子線程,調用 start () 方法
    當子線程別創建出來之后,父子線程之間的通信可以通過信號槽的方式,注意事項:
  • 在 Qt 中在子線程中不要操作程序中的窗口類型對象,不允許,如果操作了程序就掛了
  • 只有主線程才能操作程序中的窗口對象,默認的線程就是主線程,自己創建的就是子線程

2.3 實例

現在我們來完成一個功能,就是先隨機生成很多隨機數,然后通過冒泡排序,和快速排序的方法去執行,並且顯示出來

  1. 首先畫出一個窗口
    在這里插入圖片描述
    長這個樣子

  2. 創建線程類 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);
    }
}

  1. 在主窗口類中實現相關功能

說明:

  • 頭文件中定義了信號函數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();
    });

  1. 運行結果
    在這里插入圖片描述
    在這里插入圖片描述
    可以發現,在對一萬個隨機數進行排序時,快速排序要比冒泡排序快很多

2.4 多線程使用方式二

Qt 提供的第二種線程的創建方式彌補了第一種方式的缺點,用起來更加靈活,但是這種方式寫起來會相對復雜一些,其具體操作步驟如下:

  1. 創建一個新的類,讓這個類從 QObject 派生
  2. 在這個類中添加一個公共的成員函數,函數體就是我們要子線程中執行的業務邏輯
  3. 在主線程中創建一個 QThread 對象,這就是子線程的對象
  4. 在主線程中創建工作的類對象(千萬不要指定給創建的對象指定父對象)
  5. 將 MyWork 對象移動到創建的子線程對象中,需要調用 QObject 類提供的 moveToThread() 方法
  6. 啟動子線程,調用 start(), 這時候線程啟動了,但是移動到線程中的對象並沒有工作
  7. 調用 MyWork 類對象的工作函數,讓這個函數開始執行,這時候是在移動到的那個子線程中運行的

使用這種多線程方式,假設有多個不相關的業務流程需要被處理,那么就可以創建多個類似於 MyWork 的類,將業務流程放多類的公共成員函數中,然后將這個業務類的實例對象移動到對應的子線程中 moveToThread() 就可以了,這樣可以讓編寫的程序更加靈活,可讀性更強,更易於維護。

2.5 方式一與方式二區別

  • 方式一需要重載run()函數,run()函數不能帶有參數,使得我們獲取參數只能通過信號槽的方式去解決
  • 方式二更加靈活,將一個方法可以放到同一個線程中,也可以放到不同的線程中

3. 線程池

3.1 線程池原理

我們使用線程的時候就去創建一個線程,這樣實現起來非常簡便,但是就會有一個問題:如果並發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因為頻繁創建線程和銷毀線程需要時間。

那么有沒有一種辦法使得線程可以復用,就是執行完一個任務,並不被銷毀,而是可以繼續執行其他的任務呢?

線程池是一種多線程處理形式,處理過程中將任務添加到隊列,然后在創建線程后自動啟動這些任務。線程池線程都是后台線程。每個線程都使用默認的堆棧大小,以默認的優先級運行,並處於多線程單元中。如果某個線程在托管代碼中空閑(如正在等待某個事件), 則線程池將插入另一個輔助線程來使所有處理器保持繁忙。如果所有線程池線程都始終保持繁忙,但隊列中包含掛起的工作,則線程池將在一段時間后創建另一個輔助線程但線程的數目永遠不會超過最大值。超過最大值的線程可以排隊,但他們要等到其他線程完成后才啟動。

在各個編程語言的語種中都有線程池的概念,並且很多語言中直接提供了線程池,作為程序猿直接使用就可以了,下面給大家介紹一下線程池的實現原理:

線程池的組成主要分為 3 個部分,這三部分配合工作就可以得到一個完整的線程池:

  1. 任務隊列,存儲需要處理的任務,由工作的線程來處理這些任務
    1)通過線程池提供的 API 函數,將一個待處理的任務添加到任務隊列,或者從任務隊列中刪除
    2)已處理的任務會被從任務隊列中刪除
    3)線程池的使用者,也就是調用線程池函數往任務隊列中添加任務的線程就是生產者線程

  2. 工作的線程(任務隊列任務的消費者) ,N 個
    1) 線程池中維護了一定數量的工作線程,他們的作用是是不停的讀任務隊列,從里邊取出任務並處理
    2) 工作的線程相當於是任務隊列的消費者角色,
    3) 如果任務隊列為空,工作的線程將會被阻塞 (使用條件變量 / 信號量阻塞)
    4) 如果阻塞之后有了新的任務,由生產者將阻塞解除,工作線程開始工作

  3. 管理者線程(不處理任務隊列中的任務),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 實例

我們將之前的程序改為線程池來實現,具體的方法就是創建線程池,然后將這些線程添加到線程中

發現:
在這里插入圖片描述
圖中生成隨機數的線程地址和冒泡排序的地址是相同的,這是為什么呢?

  • 因為當生成隨機數的線程執行完之后,沒有事情做了,這個時候它就會去處理下一個任務,因此地址相同
  • 而且放入線程池當中的任務,不需要自己去釋放,而是線程池統一管理

4. 參考鏈接

https://subingwen.cn/qt/thread/


免責聲明!

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



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