引言
在前面對Qt多線程(QThread)做了詳細的分析:QT從入門到入土(五(1))——多線程(QThread) - 唯有自己強大 - 博客園 (cnblogs.com)
但是最近在做項目時候,要將一個函數單獨運行在另一個線程中,並且這個函數是私有函數,用到的數據也是私有數據,不能通過其他類訪問,這樣怎么辦?如何在Qt中的當前類創建線程?
一, 最簡單的多線程QtConcurrent::run()
- 其函數原型如下:
QFuture<T> QtConcurrent::run(Function function, ...)
QFuture<T> QtConcurrent::run(QThreadPool *pool, Function function, ...)
參數function需要外部函數:(或者lambda函數),后面也可跟外部函數的參數。
extern void func(QString str);
QtConcurrent :: run()也接受指向成員函數的指針。第一個參數必須是一個const引用或一個指向該類實例的指針。const成員函數一般傳遞 常量引用 (const reference),而非常量成員函數一般傳遞 指針 (pointer)
- 在VS環境中需要引用: #include"QtConcurrent/qtconcurrentrun.h"
簡單的說,QtConcurrent::run()函數會在一個單獨的線程中執行,並且該線程取自全局QThreadPool,該函數的返回值通過QFuture API提供。
請注意:該函數可能不會立即運行; 函數只有在線程可用時才會運行。通過QtConcurrent::run()返回的QFuture不支持取消、暫停,返回的QFuture只能用於查詢函數的運行/完成狀態和返回值。
- 實例:實現多線程耗時操作(調用成員函數)
在主程序threadtest.h中聲明成員函數(並添加引用)
#include <QtWidgets/QMainWindow> #include "ui_threadtest.h" #include"workThread.h" #include"qthread.h" #include"QtConcurrent/qtconcurrentrun.h"//QtConcurrent引用 #pragma execution_character_set("utf-8") class Threadtest : public QMainWindow { Q_OBJECT public: Threadtest(QWidget *parent = Q_NULLPTR); private: Ui::ThreadtestClass ui; void work();//成員函數 };
在主程序threadtest.cpp中調用成員函數
#include "threadtest.h" #include"qdebug.h" Threadtest::Threadtest(QWidget* parent) : QMainWindow(parent) { ui.setupUi(this); connect(ui.btn_start, &QPushButton::clicked, [=]() {
//將成員函數放入參數中(成員函數需要引用指針) QtConcurrent::run(this, &Threadtest::work); }); } void Threadtest::work() { qDebug() << "子線程運行:" << QThread::currentThreadId(); QThread::sleep(5); qDebug() << "子線程結束:" << QThread::currentThreadId(); }
點擊按鈕即可觸發子線程運行。
- 實例:實現多線程耗時操作(調用外部函數,帶參數)
在主程序threadtest.h中聲明外部函數
#include <QtWidgets/QMainWindow> #include "ui_threadtest.h" #include"workThread.h" #include"qthread.h" #include"QtConcurrent/qtconcurrentrun.h"//QtConcurrent引用 #pragma execution_character_set("utf-8") class Threadtest : public QMainWindow { Q_OBJECT public: Threadtest(QWidget *parent = Q_NULLPTR); private: Ui::ThreadtestClass ui; }; extern void func(QString str);//外部函數
在主程序threadtest.cpp中調用外部函數
#include "threadtest.h" #include"qdebug.h" Threadtest::Threadtest(QWidget* parent) : QMainWindow(parent) { ui.setupUi(this); connect(ui.btn_start, &QPushButton::clicked, [=]() { QtConcurrent::run(func,QString("extern")); }); } void func(QString str) { qDebug() << "子線程運行:" << QThread::currentThreadId()<< str; QThread::sleep(5); qDebug() << "子線程結束:" << QThread::currentThreadId()<< str; }
二,線程池
2.1 線程池原理
我們使用線程的時候就去創建一個線程,這樣實現起來非常簡便,但是就會有一個問題:如果並發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因為頻繁創建線程和銷毀線程需要時間。
那么有沒有一種辦法使得線程可以復用,就是執行完一個任務,並不被銷毀,而是可以繼續執行其他的任務呢?
線程池是一種多線程處理形式,處理過程中將任務添加到隊列,然后在創建線程后自動啟動這些任務。線程池線程都是后台線程。每個線程都使用默認的堆棧大小,以默認的優先級運行,並處於多線程單元中。如果某個線程在托管代碼中空閑(如正在等待某個事件), 則線程池將插入另一個輔助線程來使所有處理器保持繁忙。如果所有線程池線程都始終保持繁忙,但隊列中包含掛起的工作,則線程池將在一段時間后創建另一個輔助線程但線程的數目永遠不會超過最大值。超過最大值的線程可以排隊,但他們要等到其他線程完成后才啟動。
2.2 QRunnable
在 Qt 中使用線程池需要先創建任務,添加到線程池中的每一個任務都需要是一個 QRunnable 類型,因此在程序中需要創建子類繼承 QRunnable 這個類,然后重寫 run() 方法,在這個函數中編寫要在線程池中執行的任務,並將這個子類對象傳遞給線程池,這樣任務就可以被線程池中的某個工作的線程處理掉了。
// 在子類中必須要重寫的函數, 里邊是任務的處理流程 [pure virtual] void QRunnable::run(); // 參數設置為 true: 這個任務對象在線程池中的線程中處理完畢, 這個任務對象就會自動銷毀 // 參數設置為 false: 這個任務對象在線程池中的線程中處理完畢, 對象需要程序猿手動銷毀 void QRunnable::setAutoDelete(bool autoDelete); // 獲取當前任務對象的析構方式,返回true->自動析構, 返回false->手動析構 bool QRunnable::autoDelete() const;
例如:創建一個要添加到線程池中的任務類,處理方式如下
class MyWork : public QObject, public QRunnable { Q_OBJECT public: explicit MyWork(QObject *parent = nullptr) { // 任務執行完畢,該對象自動銷毀 setAutoDelete(true); } ~MyWork(); void run() override{} }
在上面的示例中 MyWork 類是一個多重繼承,如果需要在這個任務中使用 Qt 的信號槽機制進行數據的傳遞就必須繼承 QObject 這個類,如果不使用信號槽傳遞數據就可以不繼承了,只繼承 QRunnable 即可。
2.3 QThreadPool
Qt 中的 QThreadPool 類管理了一組 QThreads, 里邊還維護了一個任務隊列。QThreadPool 管理和回收各個 QThread 對象,以幫助減少使用線程的程序中的線程創建成本。每個Qt應用程序都有一個全局 QThreadPool 對象,可以通過調用 globalInstance() 來訪問它。也可以單獨創建一個 QThreadPool 對象使用。
線程池常用的 API 函數如下:
// 獲取和設置線程中的最大線程個數 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() 方法就可以將一個任務添加到線程池中,這個任務就可以被線程池內部的線程池處理掉了,使用線程池比自己創建線程的這種多種多線程方式更加簡單和易於維護。
具體的使用方式如下:
- mywork.h
class MyWork :public QRunnable { Q_OBJECT public: explicit MyWork(); ~MyWork(); void run() override; }
- mywork.cpp
MyWork::MyWork() : QRunnable() { // 任務執行完畢,該對象自動銷毀 setAutoDelete(true); } void MyWork::run() { // 業務處理代碼 ...... }
- mainwindow.cpp
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); // 線程池初始化,設置最大線程池數 QThreadPool::globalInstance()->setMaxThreadCount(4); // 添加任務 MyWork* task = new MyWork; QThreadPool::globalInstance()->start(task); }
由此看見,線程池的操作和QThread的run重寫類似。使用線程池可以最大限度的利用線程,減少資源的浪費
比如我們需要同時處理三個事件:生成隨機數,冒泡排序,快速排序(先生成隨機數,然后再對其同時進行冒泡排序和快速排序)
用QThread的run重寫的話,需要開啟三個線程。
但是用線程池的話,它可以根據事件的長短,(將生成隨機數和冒泡排序放在同一個線程去處理)只需要開啟兩個線程即可。