Qt 系统相关 - 多线程
Qt 多线程
Qt 多线程概述
Qt 中的多线程和 Linux 中的线程,本质是一个东西!
Linux 谈到的各种和线程线管的原理和注意事项都是在 Qt 中适用的!
在 Qt 中,多线程的处理一般是通过 QThread 类来实现。
QThread 要想创建线程,就需要创建出这样类的实例。
创建线程的时候,需要重点指定线程的入口函数。
创建一个 QThread 的子类,重写其中的 run 函数,起到指定入口函数的方式。(多态)
C++ 中这种做法,不算特别常见。相比之下 std::thread 直接指定回调的方式更常见一些。
有些 C++ 的大佬,认为多态机制,带来运行时的额外开销(运行时,查询虚函数表,找到对应的函数再执行)
性能从来不是 Qt 优先的考量
QThread 代表一个在应用程序中可以独立控制的线程,也可以和进程中的其他线程共享数据
QThread 对象管理程序中的一个控制线程。
QThread 常用 API
API | 描述 | 返回类型 | 参数类型 |
---|---|---|---|
run() | 线程的入口函数。我们子类继承来重写! | void | 无 |
start() | 通过调用 run() 开始执行线程。操作系统将根据优先级参数调度线程。如果线程已经在运行,这个函数什么也不做。 | void | 无 |
currentThread() | 返回一个指向管理当前执行线程的 QThread 的指针。 | QThread* | 无 |
isRunning() | 如果线程正在运行则返回 true,否则返回 false。 | bool | 无 |
sleep() | 使线程休眠,单位为秒。 | void | unsigned int |
msleep() | 使线程休眠,单位为毫秒。 | void | unsigned long |
usleep() | 使线程休眠,单位为微秒。 | void | unsigned long |
wait() | 阻塞线程,直到满足以下任何一个条件: | bool | unsigned long |
terminate() | 终止线程的执行。线程可以立即终止,也可以不立即终止,这取决于操作系统的调度策略。在 terminate() 之后使用 QThread::wait() 来确保。 | void | 无 |
finished() | 当线程结束时会发出该信号,可以通过该信号来实现线程的清理工作。 | void | 无 |
使用线程
创建线程的步骤:
自定义一个类,继承于 QThread,并且只有一个线程处理函数(和主线程不是同一个线程),这个线程处理函数主要就是重写父类中的 run() 函数。
线程处理函数里面写入需要执行的复杂数据处理;
启动线程不能直接调用 run() 函数,需要使用对象来调用 start() 函数实现线程启动;
线程处理函数执行结束后可以定义一个信号来告诉主线程;
最后关闭线程。
示例:
1. 首先新建 Qt 项目,设计 UI 界面如下:
2. 新建一个类,继承于 QThread 类:
程序如下:
#include <QThread> // 添加头文件class TimeThread : public QThread
{Q_OBJECTpublic:TimeThread();void run(); // 线程任务函数signals:void sendTime(QString Time); // 声明信号函数
};
// widget.h
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include <timethread.h> // 添加头文件QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();private slots:void on_btn_clicked(); // 按钮点击槽函数private:Ui::Widget *ui;TimeThread t; // 定义线程对象
};
#endif // WIDGET_H
// timethread.cpp
#include "timethread.h"
#include <QTime>
#include <QDebug>TimeThread::TimeThread()
{
}void TimeThread::run()
{while(1){QString time = QTime::currentTime().toString("hh:mm:ss");qDebug() << time;emit sendTime(time); // 发送信号sleep(1);}
}
注意:
void Thread::run()
{
// 在这个 run 中,我们是否能够直接修改界面内容呢?
// 不可以的!!!因为存在线程安全问题,多个线程同时对于界面的状态进行修改,此时就会导致界面就会出错了
// Qt 选择了一刀切!针对界面的控件状态进行任何修改,务必在主线程中执行
}
// widget.cpp
#include "widget.h"
#include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);connect(&t,&TimeThread::sendTime,this,&Widget::showTime);Widget::~Widget(){delete ui;}
}void Widget::on_btn_clicked()
{t.start(); // 开启线程
}void Widget::showTime(QString Time)
{ui->label->setText(Time);
}
执行效果:
说明:
- 线程函数内部不允许操作 UI 图形界面,一般用数据处理;
- connect() 函数第五个参数表示的为连接的方式且只有在多线程的时候才有意义!
connect() 函数第五个参数为 Qt::ConnectionType,用于指定信号和槽的连接类型。可影响信号的传递方式和槽函数的执行顺序。Qt::ConnectionType 提供了以下五种方式:
连接类型 | 描述 | 适用场景 | 注意事项 |
---|---|---|---|
Qt::AutoConnection | 根据信号和槽所在的线程自动选择连接类型。 | 信号和槽函数在同一线程中使用 Qt::DirectConnection;在不同线程中使用 Qt::QueuedConnection。 | 无 |
Qt::DirectConnection | 信号发出时,槽函数会立即在同一线程中执行。 | 适用于信号和槽函数在同一线程中的情况,可以实现直接的函数调用。 | 需要注意线程安全性。 |
Qt::QueuedConnection | 信号发出时,槽函数会被插入到接收对象所属的线程的事件队列中。 | 适用于信号和槽函数在不同线程中的情况,可以确保线程安全。 | 无 |
Qt::BlockingQueuedConnection | 发送信号的线程会被阻塞,直到槽函数执行完毕。 | 适用于需要等待槽函数执行完毕再继续的场景。 | 需要注意可能引起线程死锁的风险。 |
Qt::UniqueConnection | 这是一个标志,可以使用位或与上述任何一种连接类型组合使用。 | 可以使用位或与上述任何一种连接类型组合使用。 | 无 |
这些连接类型帮助开发者根据具体的线程情况选择合适的信号和槽连接方式,以确保程序的正确性和线程安全。
应用场景
之前学习多线程,主要还是站在服务器开发的角度来看待的。
当时谈到的多线程,最主要的目的是为了充分利用多核 CPU 的计算资源、双路 CPU(一个主板上面有两个 CPU)。
客户端,多线程仍然非常有意义,侧重点就不同了。
对于普通用户来说,“使用体验”是一个非常重要的话题~~
如果“非常快”的代价是“系统很卡”用户大概率是不会买账的。虽然普通用户的家用 PC 上也是多核 CPU~~
客户端上的程序很少会使用多线程把 CPU 计算资源吃完……
相比之下,客户端中的多线程,主要是用于,通过多线程的方式,执行一些耗时的等待 IO 的操作,避免主线程被卡死,避免对用户造成一些不好的体验。
客户端经常会和服务器进行网络通信。 比如客户端要上传/下载一个很大的文件,传输需要消耗好久(20分钟)
客户端要上传/下载一个很大的文件这就使一种密集的 IO 操作。 (比如代码中持续不断的进行 QFile.write)
这种密集 IO 就会使程序被系统阻塞,挂起~~
一旦进程都被挂起了,此时意味着,用户进行的各种操作,程序都无法响应。
因此,相比之下,更好的做法,使用单独的线程,来处理这种密集的 IO 操作。
要挂起也是挂起这个新的线程。主线程要负责事件循环,负责处理用户的各种操作~~ 此时主线程仍然可以继续工作,继续响应用户的各种操作。
线程安全
实现线程互斥和同步常用的类有:
互斥锁:QMutex、QMutexLocker
条件变量:QWaitCondition
信号量:QSemaphore
读写锁:QReadLocker、QWriteLocker、QReadWriteLock
互斥锁
互斥锁是一种保护和防止多个线程同时访问同一对象实例的方法,在 Qt 中,互斥锁主要是通过 QMutex 类来处理。
QMutex
特点:QMutex 是 Qt 框架提供的互斥锁类,用于保护共享资源的访问,实现线程间的互斥操作。
用途:在多线程环境下,通过互斥锁来控制对共享数据的访问,确保线程安全。
QMutex mutex;
mutex.lock(); // 上锁
// 访问共享资源
mutex.unlock(); // 解锁
我们先来看一个线程安全问题的代码:
int Thread::num = 0;Thread::Thread() {}void Thread::run()
{for(int i= 0; i < 50000; ++i){num++;}
}
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);Thread t1;Thread t2;t1.start();t2.start();// 由于三个线程之间是并发执行的关系,当t1和t2运行起来之后,主线程仍然会继续向后执行,执行到打印的时候,大概率t1,t2还没有执行完t1.wait();t2.wait();qDebug() << Thread::num;
}
我们需要加锁来保证线程安全!!!
// static QMutex mutex;//多个线程加锁的对象,得是同一个!void Thread::run()
{mutex.lock();for(int i= 0; i < 50000; ++i){num++;}mutex.unlock();
}
QMutexLocker
特点:QMutexLocker 是 QMutex 的辅助类,使用 RAII(Resource Acquisition Is Initialization)方式对互斥锁进行上锁和解锁操作,避免忘记解锁导致的死锁等问题。
用途:简化对互斥锁的上锁和解锁操作,避免忘记解锁导致的死锁等问题。
QMutex mutex;
{QMutexLocker locker(&mutex); // 在作用域内自动上锁// 访问共享资源
} // 在作用域结束时自动解锁
void Thread::run()
{QMutexLocker locker(&mutex);for(int i= 0; i < 50000; ++i){num++;}
}
QReadWriteLock、QReadLocker、QWriteLocker
特点:
QReadWriteLock 是读写锁类,用于控制读和写的并发访问。
QReadLocker 用于读操作上锁,允许多个线程同时读取共享资源。
QWriteLocker 用于写操作上锁,只允许一个线程写入共享资源。
用途:在某些情况下,多个线程可以同时读取共享数据,但只有一个线程能够进行写操作。读写锁提供了更高效的并发访问方式。
QReadWriteLock rwlock;
// 在读操作中使用读锁
{QReadLocker locker(&rwlock);// 读取共享资源
}
// 在写操作中使用写锁
{QWriteLocker locker(&rwlock);// 修改共享资源
}
条件变量
多个线程之间的调度是无序的,为了能够一定程度的干预线程之间的执行顺序,引入了条件变量!
在多线程编程中,假设除了等待操作系统正在执行的线程之外,某个线程还必须等待某些条件满足才能执行,这时就会出现问题。这种情况下,线程会很自然地使用锁的机制来阻塞其他线程。因为这只是线程的轮流使用,并且该线程等待某些特定条件,人们会认为需要等待条件的线程,在释放互斥锁或读写锁之后进入了睡眠状态,这样其他线程就可以继续运行。当条件满足时,等待条件的线程将被另一个线程唤醒。
在 Qt 中,专门提供了 QWaitCondition 类来解决像上述这样的问题。
特点:QWaitCondition 是 Qt 框架提供的条件变量类,用于线程之间的消息通信和同步。
用途:在某个条件满足时等待或唤醒线程,用于线程的同步和协调。
QMutex mutex;
QWaitCondition condition;// 在等待线程中
mutex.lock();
// 检查条件是否满足,若不满足则等待
while (!conditionFullFilled()) {condition.wait(&mutex); // 等待条件满足并释放锁
}
// 条件满足后继续执行
// ...
mutex.unlock();// 在改变条件的线程中
mutex.lock();
// 改变条件
changeCondition();
condition.wakeAll(); // 唤醒等待的线程
mutex.unlock();
信号量
有时在多线程编程中,需要确保多个线程可以相应的访问一个数量有限的相同资源。例如,运行程序的设备可能是非常有限的内存,因此我们更希望需要大量内存的线程将这一事实考虑在内,并根据可用的内存数量进行相关操作,多线程编程中类似问题通常用信号量来处理。信号量类似于增强的互斥锁,不仅能完成上锁和解锁操作,而且可以跟踪可用资源的数量。
特点:QSemaphore 是 Qt 框架提供的计数信号量类,用于控制同时访问共享资源的线程数量。
用途:限制并发线程数量,用于解决一些资源有限的问题。
QSemaphore semaphore(2); // 同时允许两个线程访问共享资源// 在需要访问共享资源的线程中
semaphore.acquire(); // 尝试获取信号量,若已满则阻塞
// 访问共享资源
semaphore.release(); // 释放信号量