往期鏈接:
- 《QThread源碼淺析》
- 《子類化QThread實現多線程》
- 《子類化QObject+moveToThread實現多線程》
- 本文章實例的源碼地址:https://gitee.com/CogenCG/QThreadExample.git
繼承QRunnable+QThreadPool實現多線程的方法個人感覺使用的相對較少,在這里只是簡單介紹下使用的方法。我們可以根據使用的場景來選擇方法。
此方法和QThread的區別:
- 與外界通信方式不同。由於QThread是繼承於QObject的,但QRunnable不是,所以在QThread線程中,可以直接將線程中執行的結果通過信號的方式發到主程序,而QRunnable線程不能用信號槽,只能通過別的方式,等下會介紹;
- 啟動線程方式不同。QThread線程可以直接調用start()函數啟動,而QRunnable線程需要借助QThreadPool進行啟動;
- 資源管理不同。QThread線程對象需要手動去管理刪除和釋放,而QRunnable則會在QThreadPool調用完成后自動釋放。
接下來就來看看QRunnable的用法、使用場景以及注意事項;
一、步驟
要使用QRunnable創建線程,步驟如下:
- 繼承QRunnable。和QThread使用一樣, 首先需要將你的線程類繼承於QRunnable;
- 重寫run函數。還是和QThread一樣,需要重寫run函數;
- 使用QThreadPool啟動線程。
二、實例
繼承於QRunnable的類:
#ifndef INHERITQRUNNABLE_H
#define INHERITQRUNNABLE_H
#include <QRunnable>
#include <QWidget>
#include <QDebug>
#include <QThread>
class CusRunnable : public QRunnable
{
public:
explicit CusRunnable(){
}
~CusRunnable(){
qDebug() << __FUNCTION__;
}
void run(){
qDebug() << __FUNCTION__ << QThread::currentThreadId();
QThread::msleep(1000);
}
};
#endif // INHERITQRUNNABLE_H
主界面類:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "ui_mainwindow.h"
#include "InheritQRunnable.h"
#include <QThreadPool>
#include <QDebug>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0) :
QMainWindow(parent),
ui(new Ui::MainWindow){
ui->setupUi(this);
m_pRunnable = new CusRunnable();
qDebug() << __FUNCTION__ << QThread::currentThreadId();
QThreadPool::globalInstance()->start(m_pRunnable);
}
~MainWindow(){
qDebug() << __FUNCTION__ ;
delete ui;
}
private:
Ui::MainWindow *ui;
CusRunnable * m_pRunnable = nullptr;
};
#endif // MAINWINDOW_H
直接運行以上實例,結果輸出如下:
MainWindow 0x377c
run 0x66ac
~CusRunnable
我們可以看到這里打印的線程ID是不同的,說明是在不同線程中執行,而線程執行完后就自動進入到析構函數中, 不需要手動釋放。
三、啟動線程的方式
上面我們說到要啟動QRunnable線程,需要QThreadPool配合使用,而調用方式有兩種:全局線程池和非全局線程池。
(1)使用全局線程池啟動
QThreadPool::globalInstance()->start(m_pRunnable);
(2)使用非全局線程池啟動
該方式可以控制線程最大數量, 以及其他設置,比較靈活,具體參照幫助文檔。
QThreadPool threadpool;
threadpool.setMaxThreadCount(1);
threadpool.start(m_pRunnable);
四、如何與外界通信
前面我們提到,因為QRunnable沒有繼承於QObject,所以沒法使用信號槽與外界通信,那么,如果要在QRunnable線程中和外界通信怎么辦呢,通常有兩種做法:
- 使用多繼承。讓我們的自定義線程類同時繼承於QRunnable和QObject,這樣就可以使用信號和槽,但是多線程使用比較麻煩,特別是繼承於自定義的類時,容易出現接口混亂,所以在項目中盡量少用多繼承。
- 使用QMetaObject::invokeMethod。
接下來只介紹使用QMetaObject::invokeMethod來通信:
QMetaObject::invokeMethod 函數定義如下:
static bool QMetaObject::invokeMethod(
QObject *obj, const char *member,
Qt::ConnectionType,
QGenericReturnArgument ret,
QGenericArgument val0 = QGenericArgument(Q_NULLPTR),
QGenericArgument val1 = QGenericArgument(),
QGenericArgument val2 = QGenericArgument(),
QGenericArgument val3 = QGenericArgument(),
QGenericArgument val4 = QGenericArgument(),
QGenericArgument val5 = QGenericArgument(),
QGenericArgument val6 = QGenericArgument(),
QGenericArgument val7 = QGenericArgument(),
QGenericArgument val8 = QGenericArgument(),
QGenericArgument val9 = QGenericArgument());
該函數就是嘗試調用obj的member函數,可以是信號、槽或者Q_INVOKABLE聲明的函數(能夠被Qt元對象系統喚起),只需要將函數的名稱傳遞給此函數,調用成功返回true,失敗返回false。member函數調用的返回值放在ret中,如果調用是異步的,則不能計算返回值。你可以將最多10個參數(val0、val1、val2、val3、val4、val5、val6、val7、val8和val9)傳遞給member函數,必須使用Q_ARG()和Q_RETURN_ARG()宏封裝參數,Q_ARG()接受類型名 + 該類型的常量引用;Q_RETURN_ARG()接受一個類型名 + 一個非常量引用。
QMetaObject::invokeMethod可以是異步調用,也可以是同步調用。這取決與它的連接方式Qt::ConnectionType type:
- 如果類型是Qt::DirectConnection,則會立即調用該成員,同步調用。
- 如果類型是Qt::QueuedConnection,當應用程序進入主事件循環時,將發送一個QEvent並調用該成員,異步調用。
- 如果類型是Qt::BlockingQueuedConnection,該方法將以與Qt::QueuedConnection相同的方式調用,不同的地方:當前線程將阻塞,直到事件被傳遞。使用此連接類型在同一線程中的對象之間通信將導致死鎖。
- 如果類型是Qt::AutoConnection,如果obj與調用者在同一線程,成員被同步調用;否則,它將異步調用該成員。
我們在主界面中定一個函數,用於更新界面內容:
Q_INVOKABLE void setText(QString msg){
ui->label->setText(msg);
}
繼承於QRunnable的線程類,修改完成如下:
#ifndef INHERITQRUNNABLE_H
#define INHERITQRUNNABLE_H
#include <QRunnable>
#include <QWidget>
#include <QDebug>
#include <QThread>
class CusRunnable : public QRunnable
{
public:
//修改構造函數
explicit CusRunnable(QObject *obj):m_pObj(obj){
}
~CusRunnable(){
qDebug() << __FUNCTION__;
}
void run(){
qDebug() << __FUNCTION__ << QThread::currentThreadId();
QMetaObject::invokeMethod(m_pObj,"setText",Q_ARG(QString,"hello world!")); //此處與外部通信
QThread::msleep(1000);
}
private:
QObject * m_pObj = nullptr; //定義指針
};
#endif // INHERITQRUNNABLE_H
創建線程對象時,需要將主界面對象傳入線程類,如下:
m_pRunnable = new CusRunnable(this);
到這里也就實現了線程與外部通信了,運行效果如下:
五、小結
- 使用該方法實現的多線程,線程中的資源無需用戶手動釋放,線程執行完后會自動回收資源;
- 和繼承QThread的方法一樣需要繼承類,並且重新實現run函數;
- 需要結合QThreadPool線程池來使用;
- 與外界通信可以使用如果使用信號槽機制會比較麻煩,可以使用QMetaObject::invokeMethod的方式與外界通信。
本文章實例的源碼地址:https://gitee.com/CogenCG/QThreadExample.git