Qt多线程
Qt中的多线程API参考了Java中的线程库API的设计方式,针对系统提供的线程API重新封装了。
要Qt的对线程,就需要创建一个继承自QThread的子类,通过多态重写其中的run函数,起到指定入口函数的方式。
一、QThread常用的API
run() 线程的入口函数 start() 通过调用run()开始执行线程。操作系统将根据优先级参数调度线程。如果线程已经在运行,这个函数什么也不做 currentThread() 返回一个指向管理当前执行线程的QThread的指针 isRunning() 如果线程正在运行则返回true;否则返回false sleep()/msleep()/usleep() 使线程休眠,单位为秒/毫秒/微秒 wait() 阻塞线程,直到满足以下任何一个条件:
与此QThread对象关联的线程已经完成执行(即当它从run()返回时)。如果线程已经完成,这个函数将返回true。如果线程尚未启动,它也返回true。
已经过了几毫秒。如果时间是ULONG_MAX(默认值),那么等待永远不会超时(线程必须从run()返回)。如果等待超时,此函数将返回false。这提供了与POSIX pthread_join() 函数类似的功能。
terminate() 终止线程的执行。线程可以立即终止,也可以不立即终止,这取决于操作系统的调度策略。在terminate()之后使用QThread::wait()来确保。 finished() 当线程结束时会发出该信号可以通过该信号来实现线程的清理工作。
二、计时器实例
demo: 创建一个新线程,进行一个计时器
因为Qt中仅仅允许主线程修改界面,所以只能让新线程发送信号给主线程,通过信号槽的方式
新建一个继承自QThred的类:
//Thread.h#ifndef THREAD_H #define THREAD_H#include <QWidget> #include<QThread>class Thread : public QThread {Q_OBJECT public:Thread();void run();signals:void notify(); };#endif // THREAD_H
//Thread.cpp#include "thread.h"Thread::Thread() {}void Thread::run() {//Qt中仅仅允许主线程修改界面。所以只要在Thread类中发送信号让主线程接受即可for(int i =0;i < 10;i++){//每隔一秒钟更新显示数字sleep(1);//以给主线程发送信号的形式更新emit notify();} }
//Widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include "thread.h"QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();void handle();private:Ui::Widget *ui;Thread thread; }; #endif // WIDGET_H
//Widget.cpp#include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);connect(&thread,&Thread::notify,this,&Widget::handle);//启动线程thread.start(); }Widget::~Widget() {delete ui; }void Widget::handle() {int value = ui->lcdNumber->intValue();value--;ui->lcdNumber->display(value); }
三、QThread的应用场景
操作系统中的多线程最主要中的目的,是为了充分利用多核CPU的计算资源;
客户端中的侧重点就不同了,对于普通用户来说“使用体验”是一个非常重要的话题,但是“非常快"的代价是“系统很卡”就得不偿失。客户端上的程序很少会使用多线程把CPU计算机“吃完”,更多的是执行一些耗时的等待IO操作,避免主线程被卡死时对用户造成不好的体验,如:客户端经常会和服务端进行网络通讯,客户端要上传/下载一个很大的文件,传输需要消耗很久的时间,就会引起被系统阻塞挂起,导致用户进行各种操作程序都无法响应,所以相比之下,使用一个新线程来处理这种IO操作,要挂起也是挂起这个新的线程,主线程用来处理事件、用户的各种操作
四、QObject::connect()中的第五个参数
connect()函数的第五个参数表示的为连接的方式且只有在多线程的时候才意义;
connect()函数第五个参数为Qt:connectionType,用于指定信号和槽的连接类型。同时影响信号的
传递方式和槽函数的执行顺序。Qt:ConnectionType提供了以下五种方式:
Qt:AutoConnection | 在Qt中,会根据信号和槽函数所在的线程自动选择连接类型。如果信号和槽函数在同一线程中,那么使用Qt:DirectConnection类型;如果它们位于不同的线程中,那么使用Qt:QueuedConnection类型。 |
Qt:DirectConnection | 当信号发出时,槽函数会立即在同一线程中执行。这种连接类型适用于信号和槽函数在同一线程中的情况,可以实现直接的函数调用,但需要注意线程安全性。 |
Qt:QueuedConnection | 当信号发出时,槽函数会被插入到接收对象所属的线程的事件队列中,等待下一次事件循环时执行。这种连接类型适用于信号和槽函数在不同线程中的情况,可以确保线程安全。 |
Qt:BlockingQueuedConnec tion | 与Qt:QueuedConnection类似,但是发送信号的线程会被阻塞,直到槽函数执行完毕,这种连接类型适用于需要等待槽函数执行完毕再继续的场景,但需要注意可能引起线程死锁的风险。 |
Qt:UniqueConnection | 这是一个标志,可以使用位或与上述任何一种连接类型组合使用。 |
五、线程安全问题(加锁)
加锁,即把多个要访问的公共资源通过锁保护起来,也就是把并发执行变成串行执行
c++11引入了std::mutex,Qt中也提供了对应的锁,对系统提供的锁进行封装(QMutex)
demo:
修改同一个公共变量
创建一个继承自QThread的类
//Thread.h#ifndef THREAD_H #define THREAD_H#include <QWidget> #include<QThread> #include<QMutex>class Thread : public QThread {Q_OBJECT public:Thread();//创建锁对象//多个线程进行加锁的对象必须是同一个对象//不同对象不会产生锁的互斥无法把并发执行变为串行执行static QMutex mutex;static int num;void run(); };#endif // THREAD_H
//Thread.cpp#include "thread.h"int Thread::num = 0;QMutex Thread::mutex;Thread::Thread() {}void Thread::run() {for(int i = 0;i < 50000;i++){mutex.lock();//num是一个两个线程访问的公共变量//如果是并发执行就可能出现第一个线程修改了一半//第二个线程也进行修改就容易出现问题(++操场对应三个cpu指令)//加了锁之后第一个线程顺利拿到锁,继续执行++,第一个线程没执行完时//第二个线程也尝试加锁就会阻塞等待//直到第一个线程释放锁,第二个线程才能从阻塞中被唤醒num++;mutex.unlock();} }
//Widget.h#ifndef WIDGET_H #define WIDGET_H#include <QWidget>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private:Ui::Widget *ui; }; #endif // WIDGET_H
//Widget.cpp#include "widget.h" #include "ui_widget.h" #include<QDebug>#include"thread.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);Thread t1;Thread t2;t1.start();t2.start();//三个线程是并发执行的,可能50000次循环还没有执行完就执行打印了//加上线程等待,让主线程等待这两个线程执行完成t1.wait();t2.wait();qDebug() << Thread::num; }Widget::~Widget() {delete ui; }
六、QMutexLocker()
实际开发中涉及到逻辑复杂的场景很容易忘记释放锁,或者unlock之前被分支语句或者抛出异常会让unlock不能被执行,c++11中引入智能指针就是解决上述问题的,对于释放锁也引入了std::lock_guard,也是借助RAll机制
Qt中也引入了该方案(QMutexLocker) ,Qt中的锁和c++标准库中的锁本质上都是封装系统提供的锁,不建议混着用
七、读写锁
QReadWriteLock是读写锁类,用于控制读和写的并发访问。
QReadLocker用于读操作上锁,允许多个线程同时读取共享资源。
QWriteLocker用于写操作上锁,只允许一个线程写入共享资源。
用途:在某些情况下,多个线程可以同时读取共享数据,但只有一个线程能够进行写操作。读写锁提供了更高效的并发访问方式。
八、条件变量
多个线程之间的调度时无序的,为了能够一定程度的干预线程之间的执行顺序而引入条件变量
Qt也是针对系统提供的原生API进行了封装(QWaitCondition)
QMutex mutex;
QWaitCondition condition;//在等待线程中
mutex.lock();//检查线程继续执行条件是否成立,若不满⾜则wait等待
//用while不用if的原因是唤醒之后需要再确认一下当前条件是否真的成立了
//因为wait可能被提前唤醒,导致信号被打断
while (!conditionFullfilled())
{//wait中会释放锁 + 等待,前提是获取到锁condition.wait(&mutex); //等待条件满⾜并释放锁
}//条件满⾜后继续执⾏
//...
mutex.unlock();
//在改变条件的线程中
mutex.lock();
//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程
mutex.unlock();
九、信号量
在多线程编程中,需要确保多个线程可以相应的访问一个数量有限的相同资源。如:运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这一事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用信号量来处理。信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量。
特点:QSemaphore是Qt提供的计数信号量类,用于控制同时访问共享资源的线程数量。
用途:限制并发线程数量,用于解决一些资源有限的问题。
QSemaphore semaphore(2); //同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞
//访问共享资源
//...
semaphore.release(); //释放信号量
//在另⼀个线程中进⾏类似操作