文章目录
-
个应用程序一般只有一个线程,一个线程内的操作时顺序执行的, 如果有某个比较消耗时间的计算或操作,比如网络通信中的文件传输,在一个线程内操作时,用户界面就可能会冻结而不能及时响应,出现假死现象
-
要使用多线程,其中一个线程处理窗口事件,其他线程进行逻辑运算,多个线程各司其职,不仅可以提高用户体验还可以提升程序的执行效率
-
Qt为多线程操作提供了完整的支持。QThread是线程类,是实现多线程操作的核心类。Qt中提供了QMutex、QwaitCondition、QMutexLocker、QReadWriteLock、QSemaphore等多种类用于实现线程之间的同步
-
Qt中主线程负责界面显示和窗口控件的数据更新,子线程(工作线程)负责逻辑业务处理和数据计算,子线程(工作线程)不能对窗口有任何操作,子线程(工作线程)可通过信号槽来将数据传递给主线程
多线程的创建
- Qt中实现了四种创建多线程的方式,主要掌握两种常见方式即可
- 继承QThread
- 继承QObject
- 继承QRunnable,配合QThreadPool实现多线程
- 使用QtConcurrent::run()
继承QThread
- QThread是QObject的子类,所以可以使用信号与槽机制
编程思路
- 创建一个QThread类的子类
cpp
class MyThread:public QThread
{
......
}
- 覆盖父类的 run() 方法,在该函数内部编写子线程要处理的具体的业务流程
cpp
class MyThread:public QThread
{
......
protected:
void run()
{
........
}
}
}
- 在主线程中创建子线程对象
cpp
MyThread * subThread = new MyThread;
- 调用 start() 方法,启动子线程
cpp
subThread->start();
- 这种在程序中添加子线程的方式是非常简单的,但是也有弊端,假设要在一个子线程中处理多个任务,所有的处理逻辑都需要写到run()函数中,这样该函数中的处理逻辑就会变得非常混乱,不太容易维护
使用moveToThread
- 使用这种多线程方式,假设有多个不相关的业务流程需要被处理,那么就可以创建多个类似于MyWork的类,将业务流程放入类的公共成员函数中,然后将这个业务类的实例对象通过moveToThread()移动到对应的子线程中就可以了,这样可以让编写的程序更加灵活,可读性更强,更易于维护
编程思路
- 创建一个新的类,让这个类从QObject派生
cpp
class MyWork:public QObject
{
.......
}
- 在这个类中添加一个公共的成员函数,函数体就是子线程中执行的业务逻辑
cpp
class MyWork:public QObject
{
public:
.......
// 函数名自己指定, 叫什么都可以, 参数可以根据实际需求添加
void working();
}
- 在主线程中创建一个QThread对象
cpp
QThread* sub = new QThread;
- 在主线程中创建工作的类对象
cpp
MyWork* work = new MyWork;
- 调用QObject类提供的moveToThread()方法将MyWork对象移动到创建的子线程对象中
cpp
work->moveToThread(sub); // 移动到子线程中工作
- 启动子线程,调用 start(), 这时候线程启动了, 但是移动到线程中的对象并没有工作
- 调用MyWork类对象的工作函数,让这个函数开始执行,这时候是在移动到的那个子线程中运行的
线程间同步
- 使用多线程的主要想法是希望它们可以尽可能并发执行提高效率, 在实际多线程应用编程中,由于多个线程的存在,线程间可能需要访问同一个变量,或者一个线程需要等待另外一个线程完成某个操作之后才产生相应的动作,这时候就要考虑线程间的同步、互斥问题
- Qt中提供给我们用于线程间同步的类主要包括:QMutex QReadWriteLock QWaitCondition QSemaphore
QMutex
- 是强制互斥的基本类。线程锁定互斥锁是为了访问共享资源。如果第二个线程在互斥锁已经锁定时试图锁定互斥锁,则第二个线程将进入休眠状态,直到第一个线程完成其任务并解锁互斥锁
cpp
//需要包含的头文件
#include <QMutex>
//定义互斥量
QMutex mutex;
//加锁操作
mutex.lock();
/*访问共享资源*/
//解锁操作
mutex.unlock();
QReadWriteLock
- 类似于QMutex,除了它区分了"读"和"写"访问。当一段数据没有被写入时,多个线程同时从中读取是安全的。
- QMutex强制多个读取线程轮流读取共享数据,而QReadWriteLock允许同时读取,从而提高了并行性。(读锁共享,写锁互斥)例如,假设有一个数据采集程序,一个线程负责采集数据到缓冲区,一个线程负责读取缓冲区的数据并显示,另一线程负责读取缓冲区数据并保存到文件
cpp
//需要包含的头文件
#include <QReadWriteLock>
int buffer[100]={0};
QReadWriteLock lock;
void WriteThread::run()
{
...
lock.lockForWrite();
get_data_write_buffer(); //写共享资源
lock.unlock();
...
}
void ShowThread::run()
{
...
lock.lockForRead();
show_file(); //读共享资源
lock.unlock();
...
}
void SaveThread::run()
{
...
lock.lockForRead();
save_buffer_tofile(); //读共享资源
lock.unlock();
...
}
- 如果WriteThread没有以lockForWrite形式锁定 lock,ShowThread和SaveThread就可以同时访问buffer
QWaitCondition
- 同步线程不是通过强制互斥,而是通过提供一个条件变量。其他原始线程等待资源被解锁,而QWaitCondition使线程等待特定条件被满足。为了让等待的线程继续,可以调用wakeOne()随机唤醒一个线程,或者调用wakeAll()同时唤醒所有线程
cpp
//解锁互斥量lockedMutex, 并阻塞等待唤醒条件; 被唤醒后锁定lockedMutex并退出该函数
bool wait(QMutex *lockedMutex, unsigned long time = ULONG_MAX)
bool wait(QReadWriteLock *lockedReadWriteLock, unsigned long time = ULONG_MAX)
//唤醒所有处于等待状态的线程, 唤醒的顺序不确定
void wakeAll();
//唤醒一个处于等待状态的线程,唤醒哪个不确定
void wakeOne()
- 上面的例子中,如果采集线程没有向buffer中写入数据,而显示和保存线程就显示或者向文件中写入,那操作的就是"脏数据"。逻辑上应该是采集线程向buffer中写入了数据,其它两个线程再去读,这时候就可以使用QWaitCondition来实现了
cpp
#include <QReadWriteLock>
#include <QWaitCondition>
static int buffer[100] = {0};
static int data_count = 0; // 当前数据数量
static QReadWriteLock lock;
static QWaitCondition newDataAvailable;
// 写线程
void WriteThread::run()
{
// 生产数据...
lock.lockForWrite();
// 写入共享资源
get_data_write_buffer();
data_count++; // 增加数据计数
lock.unlock();
// 唤醒所有等待的读线程
newDataAvailable.wakeAll();
}
// 显示线程
void ShowThread::run()
{
lock.lockForRead();
// ✅ 循环检查条件:确保有数据可读
while (data_count == 0) {
newDataAvailable.wait(&lock); // 等待数据
}
// 现在 data_count > 0,可以安全读取
show_file();
// 注意:如果这里消费了数据,需要减少 data_count
// 但这里只是读取,不消费,所以不需要修改
lock.unlock();
}
// 保存线程
void SaveThread::run()
{
lock.lockForRead();
// ✅ 同样需要 while 循环
while (data_count == 0) {
newDataAvailable.wait(&lock);
}
save_buffer_tofile();
lock.unlock();
}
QSemaphore
- 是QMutex的一个推广,它保护了一定数量的相同资源。相反,QMutex只保护一个资源
- 比如信号量的一个典型应用:同步生产者和消费者之间对循环缓冲区的访问
- Semaphore提供两种基本的操作,acquire() and release()
cpp
//尝试获得n个资源, 如果没有这么多资源, 线程阻塞直到有n个资源可用
void acquire(int n = 1);
//返回可用资源个数,这个是一个非负整数,为0代表当前没有资源可用
int available() const;
//释放n个资源,增加可用资源个数
void release(int n = 1)
cpp
#include <QtCore/QCoreApplication>
#include <QSemaphore>
#include <QThread>
#include <iostream>
#include <QRandomGenerator>
static int DataSize=100;
static QSemaphore goods (0);
//生产者
class Producer:public QThread
{
protected:
void run();
};
void Producer::run(){
for (int i = 0;i < DataSize;++i)
{
goods.release(1);
std::cerr<<"P";
QThread::msleep(static_cast<unsigned long>(QRandomGenerator::global()->bounded(100,150)));
}
}
//消费者
class Consumer:public QThread
{
protected:
void run();
};
void Consumer::run(){
for (int i = 0;i < DataSize;++i)
{
goods.acquire(1);
std::cerr<<"C";
QThread::msleep(static_cast<unsigned long>(QRandomGenerator::global()->bounded(100,200)));
}
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return a.exec();
}
综合demo
- warehourse.h
cpp
#ifndef WAREHOUSE_H
#define WAREHOUSE_H
#include <QMutex>
#include <QWaitCondition>
#include <QStringList>
class WareHouse
{
public:
WareHouse(int capacity=5);
~WareHouse();
private:
char *buffer ; //记录仓库的存储空间
int capacity; //仓库容量
int size; //现存商品数量
int readPos, writePos; //读写位置
public:
QMutex mutex;
QWaitCondition notEmpty;
QWaitCondition notFull;
bool full();
bool empty();
void put_goods(char c); //向仓库中放商品
char get_goods(); //从仓库中取商品
/*获取仓库中所有的商品 但不删除商品*/
QStringList peek_goods();
};
#endif // WAREHOUSE_H
- warehouse.cpp
cpp
#include "warehouse.h"
WareHouse::WareHouse(int cap)
{
capacity = cap;
buffer = new char[cap];
size = 0;
readPos = 0;
writePos = 0;
}
WareHouse::~WareHouse()
{
delete []buffer;
}
bool WareHouse::full()
{
return size == capacity;
}
bool WareHouse::empty()
{
return !size;
}
void WareHouse::put_goods(char c)
{
buffer[writePos++] = c;
size++;
if(writePos == capacity)
writePos = 0;
}
char WareHouse::get_goods()
{
char c = buffer[readPos++];
size--;
if(readPos == capacity)
readPos=0;
return c;
}
QStringList WareHouse::peek_goods()
{
QStringList list;
int pos = readPos;
QString c;
for(int i=0; i<size; i++){
c = buffer[pos++];
if(pos==capacity)
pos = 0;
list << c;
}
return list;
}
- producer.h
cpp
#ifndef PRODUCER_H
#define PRODUCER_H
#include <QThread>
class Producer : public QThread
{
Q_OBJECT
public:
explicit Producer(char goods, QObject *parent = nullptr);
signals:
void product_refresh(const QString &);
protected:
void run();
private:
char start_goods; //a b c ; A B C ...
};
#endif // PRODUCER_H
- producer.cpp
cpp
#include "producer.h"
#include "warehouse.h"
#include <QDebug>
#include <QDateTime>
#include <QRandomGenerator>
extern WareHouse house;
Producer::Producer(char goods, QObject *parent)
: QThread{parent}
{
start_goods = goods;
}
void Producer::run(){
char goods = start_goods;
while(1){
/*获取仓库使用权限*/
house.mutex.lock();
while(house.full()) { //仓库满
qDebug()<<"生产者线程: " <<QThread::currentThreadId() <<" 阻塞等待仓库不满";
house.notFull.wait(&house.mutex);
}
house.put_goods(goods);
house.mutex.unlock(); //释放仓库的使用权力
house.notEmpty.wakeAll(); //唤醒因仓库空而睡眠的消费者线程
emit product_refresh(QString("线程%1: %2: 生产:%3")
.arg(*(static_cast<int *>(QThread::currentThreadId())))
.arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
.arg(goods));
goods++;
if(goods >= start_goods+26)
break;
QThread::sleep(QRandomGenerator::global()->bounded(1,2));
}
}
- consumer.h
cpp
#define CONSUMER_H
#include <QThread>
class Consumer : public QThread
{
Q_OBJECT
public:
explicit Consumer(QObject *parent = nullptr);
signals:
void consumer_refresh(const QString &);
protected:
void run();
};
#endif // CONSUMER_H
- consumer.cpp
cpp
#include "consumer.h"
#include "warehouse.h"
#include <QDebug>
#include <QDateTime>
#include <QRandomGenerator>
extern WareHouse house;
Consumer::Consumer(QObject *parent)
: QThread{parent}
{
}
void Consumer::run(){
while(1){
house.mutex.lock();//获取仓库的访问权限
while(house.empty()){
qDebug()<<"消费线程: "<<QThread::currentThreadId() << " 因仓库空而阻塞";
house.notEmpty.wait(&house.mutex);
}
char goods = house.get_goods();
house.mutex.unlock();//释放仓库的使用权限
house.notFull.wakeAll();
emit consumer_refresh(QString("线程%1: %2: 消费: %3")
.arg(*(static_cast<int *>(QThread::currentThreadId())))
.arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
.arg(goods));
QThread::sleep(QRandomGenerator::global()->bounded(2,3));
}
}