目录
一、继承QThread
1.基本概念
2.操作流程
二、继承QObject(推荐)
1.基本概念
2.操作流程
三、继承QRunnable,配合QThreadPool实现多线程
1.外界通信
2.QMetaObject::invokeMethod()介绍
3.QMetaObject::invokeMethod()使用方式
四、使用QtConcurrent::run()
1.基本概念
2.操作流程
3.实现案例
五,线程同步
1.低级同步原语
2.高级事件队列
一、继承QThread
1.基本概念
一个QThread类的对象管理一个子线程,自定义一个类继承自QThread,并重写虚函数run(),在run()函数里实现线程需要完成的复杂操作(注意QThread只有run函数是在新线程里的)。一般在主线程创建工作子线程,并调用start(),开始执行工作子线程的任务。start()会在内部调用run()函数,进入工作线程的事件循环,在run()函数里调用exit()或quit()可以结束线程的事件循环,或者在工作主线程里调用terminate()强制结束线程。2.操作流程
1.创建一个继承QThread线程类的子类,记得包含头文件QThread
class subThread : public QThread{ ...};
2.重写父类的虚函数run()方法,在该方法内部实现子线程需要完成的复杂业务
class subThread : public QThread{ ...protected: void run(){ //全部在这里处理子线程的复杂业务 }};
3.在主线程中创建子线程对象
subThread* st = new subThread;
4.启动子线程,调用start()方法
st->start();
二、继承QObject(推荐)
1.基本概念
如果有编写多个业务类,各不相关的业务逻辑需要被处理,就可以选择这种方式,将业务逻辑放入对应业务类的公共函数中,然后将这些业务类的实例对象移动到对应的子线程中moveToThread()就可以了,这种编写多线程的方式比第一种更加灵活,可读性也更强,更易于维护2.操作流程
1.创建一个继承QObject类的子类,记得包含头文件QObject
class subObject : public QObject{ ...}
2.在该类中添加一个公共的成员函数,主要负责子线程中要执行的业务逻辑
class subObject : public QObject{ ...public: void working(); //函数名称随意取,传入的参数根据实际需求添加}
3.在主线程中创建一个QThread对象,也就是子线程的对象
QThread* subThread = new QThread;
4.主线程中创建该类的对象(创建该类对象千万不要指定父对象)
subObject* subObj = new subObject(this); //errorsubObject* subObj = new subObject; //OK
5.将工作类subObject对象移动到创建的子线程对象中,需要调用继承自QObject类提供的 moveToThread()方法
// void moveToThread(QThread *targetThread)subObj->moveToThread(subThread);
6. 启动子线程,调用start()方法,但这时候并不会启动子线程的工作函数
subThread->start();
7.在主线程中通过信号槽调用线程类subObject对象的工作函数,这时候才会到子线程中运行该工作函数
connect(ui->pushButton,&QPushButton::clicked,subObj,&subObject::working);
参考文章 Qt中多线程的使用 | 爱编程的大丙
三、继承QRunnable,配合QThreadPool实现多线程
使用信号槽通信方式可以查看这篇文章 Qt中线程池的使用 | 爱编程的大丙
1.外界通信
QRunnable类是所有可运行对象的基类,没有继承于QObject,所以就不能使用信号槽功能与外界通信使用多继承,就是让线程类同时继承QObject和QRunnable(上述文章就使用这种方式,但不推荐,毕竟C#都是单继承的,不多解释啦),让该线程类能够支持信号槽的使用使用QMetaObject::invokeMethod()2.QMetaObject::invokeMethod()介绍
该静态函数调用 obj 对象上的成员如信号,槽名,Q_INVOKABLE声明的函数(能够被Qt元对象系统唤起,所以传入的第一个对象指针是QObject类型或QObject子类类型)。如果成员可以被调用,则返回true。如果没有这样的成员或形参不匹配,则返回false。调用可以是同步的,也可以是异步的,这取决于第三个参数类型ConnectionType:Qt::DirectConnection | 该成员将立即被调用,同步调用 |
Qt::QueuedConnection | 一旦应用程序进入主事件循环,就会发送一个QEvent并调用成员,异步调用 |
Qt::BlockingQueuedConnection | 该方法将以与Qt::QueuedConnection相同的方式被调用,除了当前线程将阻塞,直到事件被传递。使用这种连接类型在同一线程中的对象之间通信将导致死锁。 |
Qt::AutoConnection | 如果obj与调用者位于同一个线程中,则同步调用成员;否则,它将异步调用成员。 |
3.QMetaObject::invokeMethod()使用方式
1.线程类中通信的函数
Q_INVOKABLE void externalTonXin(QString info);
2.在重写虚函数run内加入该接口调用方式,参数m_obj为主线程对象指针,"externalTonXin"是要调用被Q_INVOKABLE声明的函数
QMetaObject::invokeMethod(m_obj,"externalTonXin",Q_ARG(QString,"子线程主线程通信..."));
3.在主线程创建线程对象时,需要将主界面对象(this)传入线程对象构造函数中
RunnableThread* st = new RunnableThread(this);
参考文章 Qt线程之QRunnable的使用详解
四、使用QtConcurrent::run()
1.基本概念
Qt Concurrent模块包含支持程序代码并发执行的功能,可以在不使用低级线程原语的情况下编写多线程程序,它是一个单独的模块,使用起来也非常简单,不用向上面三种那样去为了某个业务处理逻辑而编写线程类,这种方式只需要你把耗时的某个接口传入该接口就行,具有用法看下面
2.操作流程
1.在.pro文件中添加QT += concurrent
2.然后在耗时接口所在的头文件中包含QtConcurrent,并在所在源文件中调用QtConcurrent::run()
QtConcurrent::run(this,&QtConcurrentDemo::working);
3.实现案例
1.界面搭建,红色注释为个控件对象名称
2.头文件如下
#ifndef QTCONCURRENTDEMO_H#define QTCONCURRENTDEMO_H#include <QWidget>#include <QtConcurrent>#include <QDebug>#define cout qDebug() << "[" << __FUNCTION__ << "][" << __LINE__ << "]:"namespace Ui {class QtConcurrentDemo;}class QtConcurrentDemo : public QWidget{ Q_OBJECTpublic: explicit QtConcurrentDemo(QWidget *parent = nullptr); ~QtConcurrentDemo(); void working(); //耗时接口,主线程中运行会阻塞,子线程中能正常运行private: Ui::QtConcurrentDemo *ui; bool flag; //控制线程开闭};#endif // QTCONCURRENTDEMO_H
3.源文件如下
#include "qtconcurrentdemo.h"#include "ui_qtconcurrentdemo.h"QtConcurrentDemo::QtConcurrentDemo(QWidget *parent) : QWidget(parent), ui(new Ui::QtConcurrentDemo){ ui->setupUi(this); flag = false; connect(ui->startThread,&QPushButton::clicked,this,[=](){ //耗时接口 working 在子线程中运行 cout<<QThread::currentThreadId(); flag = true; QtConcurrent::run(this,&QtConcurrentDemo::working); }); connect(ui->startNotThread,&QPushButton::clicked,this,[=](){ //耗时接口 working 在主线程中运行 cout<<QThread::currentThreadId(); flag = true; working(); }); connect(ui->closeThread,&QPushButton::clicked,this,[=](){ //关闭线程 cout<<QThread::currentThreadId(); flag = false; ui->label->setNum(0); });}QtConcurrentDemo::~QtConcurrentDemo(){ delete ui;}void QtConcurrentDemo::working(){ cout<<QThread::currentThreadId(); int i=0; while (flag) { ui->label->setNum(i++); cout<<i; QThread::sleep(1); }}
五,线程同步
虽然线程的目的是允许代码并行运行,但有时线程必须停止并等待其他线程。例如,如果两个线程试图同时写入同一个变量,结果是未定义的。强迫线程相互等待的原则称为互斥。这是保护数据等共享资源的常用技术。
Qt提供了用于同步线程的低级原语和高级机制,介绍如下:
1.低级同步原语
QMutex 是强制互斥的基本类。线程锁定互斥锁是为了访问共享资源。如果第二个线程在互斥锁已经锁定时试图锁定互斥锁,则第二个线程将进入休眠状态,直到第一个线程完成其任务并解锁互斥锁。QReadWriteLock 类似于QMutex,除了它区分了“读”和“写”访问。当一段数据没有被写入时,多个线程同时从中读取是安全的。QMutex强制多个读取器轮流读取共享数据,而QReadWriteLock允许同时读取,从而提高了并行性。(读锁共享,写锁互斥)QSemaphore 是QMutex的一个推广,它保护了一定数量的相同资源。相反,QMutex只保护一个资源。比如信号量的一个典型应用:同步生产者和消费者之间对循环缓冲区的访问。QWaitCondition 同步线程不是通过强制互斥,而是通过提供一个条件变量。其他原语使线程等待资源被解锁,而QWaitCondition使线程等待特定条件被满足。为了让等待的线程继续,可以调用wakeOne()随机唤醒一个线程,或者调用wakeAll()同时唤醒所有线程。注意:Qt的这些同步类依赖于使用正确对齐的指针。例如,不能在MSVC中使用打包类。
这些同步类可用于使方法线程安全。然而,这样做会导致性能损失,这就是为什么大多数Qt方法不是线程安全的。
风险:
如果一个线程锁定了一个资源,但没有解锁它,应用程序可能会冻结,因为该资源将对其他线程永久不可用。例如,如果抛出异常并强制当前函数在不释放锁的情况下返回,就会发生这种情况。另一个类似的场景是死锁。例如,假设线程A正在等待线程B解锁一个资源。如果线程B也在等待线程A解锁不同的资源,那么两个线程将永远等待下去,因此应用程序将被冻结。方便类:
QMutexLocker,QReadLocker,QWriteLocker是方便类,它们使QMutex和QReadWriteLock的使用更容易。这些方便类都是在构造时锁定资源,在析构时自动解锁释放资源。它们被设计用来简化使用QMutex和QReadWriteLock的代码,从而降低资源被意外永久锁定的几率。