QT中锁的使用
作者:艾莉丝努力练剑
本文主要介绍了QT中锁的使用,用通俗例子讲透QMutex、QMutexLocker和QReadWriteLock,教你用RAII守卫避免锁泄漏,轻松解决num++不安全问题,提升并发性能
1 ~> Qt 锁机制与线程安全
1.1 线程安全基础
1.1.1 线程安全问题产生根源
- 多线程并发访问共享公共资源,线程调度顺序不可预测。
- 简单自增num++不是原子操作,底层对应多条 CPU 指令;线程可以在指令中间被调度切换,造成数据竞争,最终计算结果错误。
- 解决方案核心思想:加锁,将并发访问转变为串行访问。
- 底层来源
- Linux 原生:mutex互斥量
- C++11 标准库:std::mutex,对操作系统互斥量封装
- Qt:QMutex,同样封装操作系统互斥量,类命名遵循 Q 前缀规则。
1.2 QMutex 互斥锁基础使用
1.2.2 核心接口
- void lock():加锁。若锁已经被其他线程持有,调用线程阻塞等待,直到获取锁。
- void unlock():释放锁。必须由持有锁的线程调用。
1.2.3 复现线程不安全 Demo
业务场景: 两个 QThread 子线程,同时对同一个静态共享变量循环累加。 预期结果:每个线程循环 50000 次,最终结果 100000;不加锁会出现小于 100000 的错误结果。
thread.h
#ifndef THREAD_H
#define THREAD_H
#include <QThread>
class Thread : public QThread
{
Q_OBJECT
public:
Thread();
// 共享静态变量,多线程访问
static int num;
protected:
// 线程入口函数,重写run
void run() override;
};
#endif // THREAD_H
thread.cpp
#include "thread.h"
// 静态成员必须在类外初始化
int Thread::num = 0;
Thread::Thread()
{
}
void Thread::run()
{
// 不加锁,并发修改共享变量,线程不安全
for(int i = 0; i < 50000; ++i)
{
num++;
}
}
widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include "thread.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
Thread t1;
Thread t2;
t1.start();
t2.start();
// 主线程等待两个子线程执行完毕,再打印结果;不调用wait会直接打印未完成值
t1.wait();
t2.wait();
qDebug() << Thread::num;
}
Widget::~Widget()
{
delete ui;
}
现象: 输出结果小于 100000,复现数据竞争线程安全 bug。
1.2.4 使用 QMutex 解决线程安全
注意: 多线程互斥,所有线程必须操作同一个锁对象;若每个线程持有独立锁对象,无法实现互斥保护。锁对象定义为static保证全局唯一。
thread.h 修改后
#ifndef THREAD_H
#define THREAD_H
#include <QThread>
#include <QMutex>
class Thread : public QThread
{
Q_OBJECT
public:
Thread();
static int num;
// 静态互斥锁对象,所有线程共用同一把锁
static QMutex mutex;
protected:
void run() override;
};
#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++; // 临界区,访问共享资源
mutex.unlock(); // 解锁
}
}
运行结果输出固定100000,线程安全问题消除。
1.3 锁的异常风险与 RAII 守卫锁 QMutexLocker
1.3.1 手动 lock/unlock 存在的缺陷
手动配对lock() / unlock()极易出现锁泄漏(锁没有释放),场景:
- 临界区内部存在if分支return提前退出函数,unlock()不会执行。
- 临界区代码抛出异常,程序流程跳转,unlock()无法执行。
锁泄漏后果:其他线程永久阻塞等待锁,发生程序卡死。
1.3.2 RAII 思想
C++ RAII:资源获取即初始化,利用局部对象的生命周期自动管理资源。
- C++11 标准库:std::lock_guard<std::mutex>,栈对象构造时加锁,析构时解锁。
- Qt 对应封装类:QMutexLocker,功能等价,接收锁指针作为构造参数;栈局部对象,出作用域自动析构,自动执行 unlock。
1.3.3 QMutexLocker 代码示例
#include "thread.h"
#include <QMutexLocker>
int Thread::num = 0;
QMutex Thread::mutex;
Thread::Thread()
{
}
void Thread::run()
{
for(int i = 0; i < 50000; ++i)
{
// 局部栈对象,构造时lock(&mutex)
QMutexLocker locker(&mutex);
num++;
// 无需手动unlock;出循环迭代的大括号,locker局部对象析构,自动调用unlock
}
}
- 无论内部 return、异常抛出,只要离开当前作用域,局部 locker 对象析构,锁一定会释放。
1.3.4 Qt 锁与 C++ 标准锁混用说明
- Qt 锁QMutex与 C++ 标准锁std::mutex底层都封装操作系统原生互斥 API,技术上可以混用。
- 工程实践不推荐混用:接口、守卫类不统一(QMutexLocker vs std::lock_guard),提高维护成本,容易写出错误代码。
1.4 Qt 读写锁 QReadWriteLock
1.4.1 适用场景
少写多读业务场景:大量线程并发读取共享数据,极少线程修改数据。
- 普通互斥锁 QMutex:读写全部串行,读操作之间也会互相阻塞,并发性能差。
- 读写锁特性
- 读锁与读锁:不互斥,多个线程可以同时获取读锁并发读取
- 读锁与写锁:互斥;写锁与写锁:互斥
- 写操作独占资源,同一时刻只允许一个线程执行写。
1.4.2 配套守卫类
- QReadLocker:读锁 RAII 守卫,构造上读锁,析构释放读锁
- QWriteLocker:写锁 RAII 守卫,构造上写锁,析构释放写锁
1.4.3 读写锁代码示例
#include <QReadWriteLock>
QReadWriteLock rwLock;
// 读操作:多线程可并发执行
{
QReadLocker locker(&rwLock);
// 执行只读访问共享资源
} // 作用域结束自动释放读锁
// 写操作:独占访问
{
QWriteLocker locker(&rwLock);
// 修改共享资源
} // 作用域结束自动释放写锁
1.5 核心面试考点总结
- 使用 QMutex 必须保证多线程操作同一个锁实例,static 成员是常用实现方式。
- wait()作用:主线程阻塞,等待子线程执行完毕;不 wait 直接读取共享变量会读到未计算完成的中间值。
- num++不是原子操作,是线程不安全典型案例。
- 优先使用 RAII 守卫类QMutexLocker,不要裸写 lock/unlock,规避锁泄漏。
- QReadWriteLock 优势:少读多写场景提升并发吞吐量;写的时候所有读全部阻塞。
- 锁泄漏:锁加完之后没有释放,会造成其他线程永久阻塞。
- Qt 锁与 C++ 标准锁底层都是操作系统 mutex 封装,但工程禁止随意混用。
结尾
到此这篇关于QT中锁的使用的文章就介绍到这了,更多相关QT 锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
