本教程不适用于QTimer调用子线程(QTimer暂时没有找到较好的方法,除非把QTimer对象也转入子线程,还未实现)
例如:使用QTimer来获取视频帧显示到QLabel中
但是适用通过子线程while(读取帧,显示到主线程的QLabel), 十分流畅
本教程主要包括如下内容:
QThread的主要接口,信号
开启子线程的两种方法
多线程直接解决的问题就是:
窗口后台程序处理数据,导致窗口无响应或卡顿.
一般分为组件类,数据类,任务类,线程类
int main(int argc, char* argv[])
{
QApplication app(argc, argv);
// 构造主窗口对象并显示
MainWindow w;
w.show();
// 进入事件循环
return app.exec();
}
在程序初始化完成后,主线程进入main()函数开始执行应用代码。一般地,我们在主线程上构建界面对象,然后进入事件循环以处理控件绘制、用户输入、系统输出等消息。这就是我们通常说的事件驱动模型。
主线程承担着用户交互的重任,当在主线程上运行费时的代码时,就会影响用户的正常操作。所以我们常把一些费时费力的计算工作移出主线程,开辟新的线程来运行之。
QThread是QT中用于线程管理的类,调用一个QThread对象的start()方法时,会创建一个新的线程并执行它的run()方法。默认地,run()会调用exec()方法进入自己的消息循环中。如下图所示:
上图中有主线程、工作线程都是执行事件循环,并且注意到主线程内部含有thr、w、objs这些QObject对象(这些对象都是在主线程上创建的)。主线程的事件循环负责检测这些对象是否有消息要处理,有的话则调用对象的slot方法。可以使用QObject::moveToThread方法将某个对象移到其他线程中,譬如:
class Worker : public QObject {
Q_OBJECT
…
}
void someFunc()
{
QThread thr = new QThread;
Worker worker = new Worker;
worker->moveToThread(thr);
thr->start();
…
}
如果在主线程上调用someFunc(),则workerThread和worker在创建后都是关联在主线程上,当调用worker->moveToThread()后,worker对象关联到了新的线程中,如图所示:
isFinished()与isRunning()
// 构造函数
QThread::QThread(QObject *parent = Q_NULLPTR);
// 判断线程中的任务是不是处理完毕了
bool QThread::isFinished() const;
// 判断子线程是不是在执行任务
bool QThread::isRunning() const;
priority()与setPriority()
// 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 --> 最高的优先级, 默认是这个
exit()与wait()
// 退出线程, 停止底层的事件循环
// 退出线程的工作函数
void QThread::exit(int returnCode = 0);
// 调用线程退出函数之后, 线程不会马上退出因为当前任务有可能还没有完成, 调回用这个函数是
// 等待任务完成, 然后退出线程, 一般情况下会在 exit() 后边调用这个函数
bool QThread::wait(unsigned long time = ULONG_MAX);
quit()与start()和terminate()
// 和调用 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();
run()
// 子线程要处理什么任务, 需要写到 run() 中
[virtual protected] void QThread::run();
这个 run() 是一个虚函数,如果想让创建的子线程执行某个任务,需要写一个子类让其继承 QThread,并且在子类中重写父类的 run() 方法,函数体就是对应的任务处理流程。
另外,这个函数是一个受保护的成员函数,不能够在类的外部调用,如果想要让线程执行这个函数中的业务流程,需要通过当前线程对象调用槽函数 start() 启动子线程,当子线程被启动,这个 run() 函数也就在线程内部被调用了。
Signals
void finished()
void started() //注意不是strat()
该方法的缺点:
假设要在一个子线程中处理多个任务,所有的处理逻辑都需要写到run()函数中,
总结run()的要点:
1⃣️复写run()方法,缺点:不能传递任何参数且维护困难
2⃣️run()方法是protected,不能外部调用
3⃣️QThread对象调用start()后自动运行run()
4⃣️在run()方法的函数结构体内部,执行strat()后必须添加.exec()
void StartMession::run()
{
qDebug() << "StartMession Thread: " << QThread::currentThread() << endl;
_mession->_version = 12.34;
_mession->printInfo();
exec();
}
基于信号槽的多线程主要是利用信号在主线程和子线程之间传送数据, 难点在于:
1⃣️发送信号的时机和信号发射的主客体;
2⃣️发送信号后如何处理传递来的参数;
3⃣️信号和处理函数的关联问题.
通过类的公有成员来实现数据交流
直接在设计类的时候,将需要在主线程和子线程中传递的变量 声明在类定义public中
设计步骤:
1.添加C++类,修改继承关系为QThread
2.在主线程和子线程中添加需要的发射的signal,并关联相关的处理函数,直接展示代码
multhread.h (子线程类头文件)
在该文件中,声明了一个信号sendResult(),用来从子线程中发送数据给主线程, 这里的成员A和saveParameterA()是从主线程获取数据后,保存获取得到的数据.(因为run()无法传递参数,所以只能通过信号传递参数后,保存到类对象成员中)
//multhread.h
#ifndef MULTHREAD_H
#define MULTHREAD_H
#include
#include
#include
class Generate : public QThread //注意这里父类
{
Q_OBJECT
public:
explicit Generate(QObject *parent = nullptr); //这里传递QObject
void saveParameterA(double);
signals:
void sendResult(double);
protected:
void run() override;
private:
double A;
};
#endif // MULTHREAD_H
multhread.cpp(子线程类实现文件)
该文件中,显然是对Generate子线程类的实现, 其中需要注意的是run()函数,该函数是对run()的重构,由于不能传递参数,所以只能用信号的方式传递,
//multhread.cpp
#include "multhread.h"
#
Generate::Generate(QObject *parent) : QThread(parent)
{
}
void Generate::saveParameterA(double sendA)
{
A = sendA;
}
void Generate::run()
{
std::cout<<"your Generate's function "<
主线程类头文件
在mainwindow.h中,声明了一个信号sendParameterA(),用来从主线程发送数据给子线程
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include "multhread.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
private:
Ui::MainWindow *ui;
signals:
void sendParameterA(double);
};
#endif // MAINWINDOW_H
mainwindow.cpp 主线程类实现文件
注意要点:
1⃣️应在子线程类对象创建好后,进行主线程信号的关联,即: 主线程信号和子线程数据处理函数关联
2⃣️主线程发射信号,然后QThread::start()启动子线程,start()启动子线程后,会自动运行run()函数,
3⃣️子线程完毕后必然返回处理结果给主线程,则必然有子线程发射信号,因此,需要关联: 子线程信号和主线程数据处理函数
//mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
this->resize(1080,720);
this->setFixedSize(this->width(),this->height());
Generate* gen = new Generate(this);
connect(this,&MainWindow::sendParameterA,gen,&Generate::saveParameterA);
connect(ui->pushButton,&QPushButton::clicked,this,[=]()
{
emit sendParameterA(0.09);
gen->start();
}
);
connect(gen,&Generate::sendResult,this,[=](double res)
{
QString result;
result = QString::number(res);
ui->plainTextEdit->appendPlainText(result);
});
}
MainWindow::~MainWindow()
{
delete ui;
}
更新类成员来实现数据传递(已跑通)
由于案例1 任务类和线程类合并成了一个类, 在后续修改任务类时难免麻烦,因此我把任务类和线程类分开了
任务类实现业务逻辑
线程类负责调用任务类到当前子线程
MainWindow主窗口,在主线程中, StartMession类重写run()在子线程t1中, 且在run()中调用Mession类对象方法,也在子线程t1中
直接上代码
MainWindow.h 创建一个线程类成员
//mainwindow.h
#pragma once
//这个案例解释了run()方法调用自定义类对象成员函数是,该成员函数仍然在主线程中
//仅仅run(){}函数体内的语句在子线程中
#include
#include "ui_MainWindow.h"
#include "Mession.h"
#include "StartMession.h"
#include
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = Q_NULLPTR);
StartMession* _startM1; //声明线程类
private:
Ui::MainWindowClass ui;
};
MainWindow.cpp 创建线程类,并启动线程类开辟子线程
//mainwindow.cpp
#include "MainWindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
qDebug() << "MainWindow Thread: " << QThread::currentThread() << endl;
_startM1 = new StartMession(this);
_startM1->start();
}
StartMession.h 线程类 添加run()函数,并声明 任务类Mession对象
//StartMession.h
#pragma once
#include
#include
#include "Mession.h"
#include
class StartMession : public QThread
{
Q_OBJECT
public:
explicit StartMession(QObject *parent = nullptr);
~StartMession();
Mession* _mession;
protected:
void run();
};
StartMession.cpp 线程类实现, 注意这father是QThread
//StartMession.cpp
#include "StartMession.h"
StartMession::StartMession(QObject *parent):QThread(parent)
{
_mession = new Mession();
}
StartMession::~StartMession()
{
}
void StartMession::run()
{
qDebug() << "StartMession Thread: " << QThread::currentThread() << endl;
//QTimer*_timer = new QTimer();
//connect(_timer, &QTimer::timeout, _mession, &Mession::printInfo);
_mession->_version = 12.34;
_mession->printInfo();
//_timer->start(2);
exec();
}
Mession.h 任务类头文件 ,run()方法调用任务类时, 任务类可以是QWidget派生类,
//Mession.h
#pragma once
#include
#include "ui_Mession.h"
#include
#include
#include
class Mession : public QWidget
{
Q_OBJECT
public:
Mession(QWidget *parent = Q_NULLPTR);
~Mession();
double _version;
void printInfo();
private:
Ui::Mession ui;
};
Mession.cpp 任务类实现文件,
//Mession.cpp
#include "Mession.h"
Mession::Mession(QWidget *parent)
: QWidget(parent)
{
ui.setupUi(this);
}
Mession::~Mession()
{
}
void Mession::printInfo()
{
int i = 0;
while (i<10)
{
qDebug() << "version: " << _version << endl;
qDebug() << "Mession current thread" << QThread::currentThread() << endl;
i++;
}
}
显然, 主线程类MainWindow调用 线程类StartMession后, 线程类和 线程类调用的任务类,都在同一子线程中执行
使用这种多线程方式,假设有多个不相关的业务流程需要被处理,那么就可以创建多个类似于 MyWork 的类,将业务流程放多类的公共成员函数中,然后将这个业务类的实例对象移动到对应的子线程中 moveToThread() 就可以了,这样可以让编写的程序更加灵活,可读性更强,更易于维护。
特点:
线程类是线程类,任务类是任务类,彼此独立,
难点在于:
1⃣️如何将任务类的成员函数放到线程中执行?(下面介绍了一种利用connect()的方法)
将任务类放到线程中很简单,只需要利用mession1->moveToThread(thread1)
关键在于如何使任务类成员函数在线程中开始执行,因为只有调用了成员函数(暂称为working()),才会执行working(),
connect()的方法就是: 利用xjh信号和working()的关联, 使得xjh信号发射后,只需启动线程,即刻完成working()的调用,从而完成了working()在子线程thread1中的执行.
2⃣️成员函数在线程中开始执行的时机问题,即什么对象发射了什么信号后,已用moveToThead()移动到线程中的任务类对象开始执行其成员函数(暂称为working())
这里的什么对象发射了什么信号, 表示了:
1. 对象可能是主窗体的组件对象,也可能是任务类对象,
2.信号可能是组件对象发射的常规信号,也可能是任务类对象自用来传递数据的自定义信号
Qt 提供的第二种线程的创建方式弥补了第一种方式的缺点,用起来更加灵活,但是这种方式写起来会相对复杂一些,其具体操作步骤如下:
1.创建一个新的任务类,让这个类从 QObject 派生, 这一点很重要,如果是继承了QWidget将无法移动到子线程中. ui文件同样不支持转移到子线程中
class MyWork:public QObject
{
.......
}
2.在这个任务类类中添加一个公共的成员函数,函数体就是我们要子线程中执行的业务逻辑
class MyWork:public QObject
{
public:
.......
// 函数名自己指定, 叫什么都可以, 参数可以根据实际需求添加
void working();
}
3.在主线程中创建一个 QThread 对象,这就是子线程的对象
QThread* sub = new QThread;
4.在主线程中创建工作的类对象(千万不要指定给创建的对象指定父对象),
如果指定了则: QObject::moveToThread: Cannot move objects with a parent
MyWork* work = new MyWork(this); // error
MyWork* work = new MyWork; // ok
5.将 MyWork 对象移动到创建的子线程对象中,需要调用 QObject 类提供的 moveToThread() 方法
// void QObject::moveToThread(QThread *targetThread);
// 如果给work指定了父对象, 这个函数调用就失败了
// 提示: QObject::moveToThread: Cannot move objects with a parent
work->moveToThread(sub); // 移动到子线程中工作
6.启动子线程,调用 start(), 这时候线程启动了,但是移动到线程中的对象并没有工作
7.调用 MyWork 类对象的成员函数working(),让这个函数开始执行,这时候是在移动到的那个子线程中运行的
难点1⃣️的解决方法:connect()方法
结合6.7.两点,在启动线程后,使任务类开始使用相关函数进行处理数据的方法如下:
主线程信号sendStartData和任务类成员函数working关联;
ui界面按钮发射信号,产生如下操作: 主线程发射信号sendStartData(0.3234),传递数据, t1线程启动,由于先前信号sendStartData和任务类成员函数working已经关联,因此sendStartData发射,t1线程启动后,则任务类对象myWork则在t1线程中执行working函数;
connect(this, &MainWindow::sendStartData,myWork,&Generate::working);
connect(ui->pb_start, &QPushButton::clicked, this,[=]()
{
emit sendStartData(0.3234);
t1->start();
};
MainWindow主线程类, Mession任务类, SubMession任务类
用Mession对象调用SubMession的成员函数进行人脸识别,并实时显示到MainWindow的QLabel中
MainWindow.h 创建了一个线程, 创建线程类_t1
//MainWindow.h
#pragma once
#include
#include "ui_MainWindow.h"
#include
#include"Mession.h"
#include
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = Q_NULLPTR);
QThread* _t1;
//Mession* _m1;
private:
Ui::MainWindowClass ui;
private slots:
void on_pushButton_clicked();
signals:
void sendData(double a);
};
MainWindow.cpp 构造函数 初始化了 一个Mession任务类,并将线程类的started信号,和任务类的成员函数 关联在一起
//MainWindow.cpp
#include "MainWindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
_t1 = new QThread;
Mession* _m1 = new Mession();
_m1->_labelM = ui.label;
_m1->moveToThread(_t1);
connect(_t1, &QThread::started, _m1, &Mession::printInfo,Qt::QueuedConnection);
//connect(this, &MainWindow::sendData,_m1, &Mession::printInfo);
//_t1->start();
}
void MainWindow::on_pushButton_clicked()
{
qDebug() << "mainWindow thread:" << QThread::currentThread() << endl;
_t1->start();
//emit sendData(1);
}
Mession.h 该任务类主要作用:
传递主线程的ui中的QLabel指针给SubMession对象,
调用SubMession的人脸识别方法
//Mession.h
#pragma once
#include
#include "ui_Mession.h"
#include
#include
#include
#include "SubMession.h"
#include
class Mession : public QObject
{
Q_OBJECT
public:
Mession(QObject *parent = Q_NULLPTR);
~Mession();
double _version;
void printInfo();
int _count = 0;
SubMession* _subM1;
QTimer* _t1;
QLabel* _labelM;
private:
Ui::Mession ui;
};
Mession.cpp 当线程类的信号发射后, 启动Mession::printInfo()
//Mession.cpp
#include "Mession.h"
Mession::Mession(QObject *parent)
:QObject(parent)
{
_subM1 = new SubMession;
}
Mession::~Mession()
{
}
void Mession::printInfo()
{
_subM1->_label = _labelM;
while (_count<3)
{
qDebug() << "Mession current thread" << QThread::currentThread() << endl;
//_subM1->printName();
_count++;
}
_subM1->printName();
}
SubMession.h 主要是用来人脸识别
//SubMession.h
#pragma once
#include
#include
#include
#include
#include
#include
#include "ConvertMatQImage.h"
#include
class SubMession : public QObject
{
Q_OBJECT
public:
SubMession(QObject *parent);
SubMession();
~SubMession();
QLabel * _label;
ConvertMatQImage* _cvt;
void printName();
};
SubMession.cpp 人脸识别的实现文件
//SubMession.cpp
#include "SubMession.h"
SubMession::SubMession(QObject *parent)
: QObject(parent)
{
}
SubMession::SubMession()
{
}
SubMession::~SubMession()
{
}
void SubMession::printName()
{
using namespace cv;
using namespace std;
cv::CascadeClassifier detector("haarcascade_frontalface_alt_tree.xml");
vector faces;
qDebug() << "SubMession Thread: " << QThread::currentThread() << endl;
VideoCapture cap(0);
Mat frame;
cap.read(frame);
QImage img;
Mat gray;
while (cap.read(frame))
{
//级联检测比较卡顿, 只输出帧非常流畅
cvtColor(frame, gray, COLOR_BGR2GRAY);
detector.detectMultiScale(frame, faces, 1.1, 2, 0, Size(20, 20), Size(1000, 1000));
for (int i = 0; i < faces.size(); i++)
{
rectangle(frame, faces[i], Scalar(0, 255, 0));
putText(frame, "Face", Point(faces[i].x, faces[i].y), FONT_HERSHEY_SCRIPT_COMPLEX,1,Scalar(255,255,255),0.5,8);
}
img = _cvt->matToQImage(&frame);
_label->setPixmap(QPixmap::fromImage(img));
}
}
利用信号槽来释放,当主线程,即mainwindow销毁时,会发射信号&MainWindow::destroyed(),利用该信号connect()完成
任务类对象也可以同时释放
QThread* t1 = new Thread();
QMyWork* w1 = new QMyWork();
connect(this, &MainWindow::destroyed(),this,[=]()
{
t1->quit();
t1->wait();
t1->deleteLater();// 等同于delete t1;
w1->deleteLater();//delete w1;
});