
个人主页艾莉丝努力练剑❄专栏传送门《C语言》《数据结构与算法》《C/C干货分享学习过程记录》《Linux操作系统编程详解》《笔试/面试常见算法从基础到进阶》《Python干货分享》⭐️为天地立心为生民立命为往圣继绝学为万世开太平 艾莉丝的简介文章目录1 ~ Qt 锁机制与线程安全1.1 线程安全基础1.1.1 线程安全问题产生根源1.2 QMutex 互斥锁基础使用1.2.2 核心接口1.2.3 复现线程不安全 Demo1.2.4 使用 QMutex 解决线程安全1.3 锁的异常风险与 RAII 守卫锁 QMutexLocker1.3.1 手动 lock/unlock 存在的缺陷1.3.2 RAII 思想1.3.3 QMutexLocker 代码示例1.3.4 Qt 锁与 C 标准锁混用说明1.4 Qt 读写锁 QReadWriteLock1.4.1 适用场景1.4.2 配套守卫类1.4.3 读写锁代码示例1.5 核心面试考点总结结尾1 ~ Qt 锁机制与线程安全1.1 线程安全基础1.1.1 线程安全问题产生根源多线程并发访问共享公共资源线程调度顺序不可预测。简单自增num不是原子操作底层对应多条 CPU 指令线程可以在指令中间被调度切换造成数据竞争最终计算结果错误。解决方案核心思想加锁将并发访问转变为串行访问。底层来源Linux 原生mutex互斥量C11 标准库std::mutex对操作系统互斥量封装QtQMutex同样封装操作系统互斥量类命名遵循 Q 前缀规则。1.2 QMutex 互斥锁基础使用1.2.2 核心接口void lock()加锁。若锁已经被其他线程持有调用线程阻塞等待直到获取锁。void unlock()释放锁。必须由持有锁的线程调用。1.2.3 复现线程不安全 Demo业务场景两个 QThread 子线程同时对同一个静态共享变量循环累加。 预期结果每个线程循环 50000 次最终结果 100000不加锁会出现小于 100000 的错误结果。thread.h#ifndefTHREAD_H#defineTHREAD_H#includeQThreadclassThread:publicQThread{Q_OBJECTpublic:Thread();// 共享静态变量多线程访问staticintnum;protected:// 线程入口函数重写runvoidrun()override;};#endif// THREAD_Hthread.cpp#includethread.h// 静态成员必须在类外初始化intThread::num0;Thread::Thread(){}voidThread::run(){// 不加锁并发修改共享变量线程不安全for(inti0;i50000;i){num;}}widget.cpp#includewidget.h#includeui_widget.h#includethread.h#includeQDebugWidget::Widget(QWidget*parent):QWidget(parent),ui(newUi::Widget){ui-setupUi(this);Thread t1;Thread t2;t1.start();t2.start();// 主线程等待两个子线程执行完毕再打印结果不调用wait会直接打印未完成值t1.wait();t2.wait();qDebug()Thread::num;}Widget::~Widget(){deleteui;}现象输出结果小于 100000复现数据竞争线程安全 bug。1.2.4 使用 QMutex 解决线程安全注意多线程互斥所有线程必须操作同一个锁对象若每个线程持有独立锁对象无法实现互斥保护。锁对象定义为static保证全局唯一。thread.h 修改后#ifndefTHREAD_H#defineTHREAD_H#includeQThread#includeQMutexclassThread:publicQThread{Q_OBJECTpublic:Thread();staticintnum;// 静态互斥锁对象所有线程共用同一把锁staticQMutex mutex;protected:voidrun()override;};#endif// THREAD_Hthread.cpp 修改后#includethread.hintThread::num0;// 静态锁对象类外定义初始化QMutex Thread::mutex;Thread::Thread(){}voidThread::run(){for(inti0;i50000;i){mutex.lock();// 加锁num;// 临界区访问共享资源mutex.unlock();// 解锁}}运行结果输出固定100000线程安全问题消除。1.3 锁的异常风险与 RAII 守卫锁 QMutexLocker1.3.1 手动 lock/unlock 存在的缺陷手动配对lock()/unlock()极易出现锁泄漏锁没有释放场景临界区内部存在if分支return提前退出函数unlock()不会执行。临界区代码抛出异常程序流程跳转unlock()无法执行。锁泄漏后果其他线程永久阻塞等待锁发生程序卡死。1.3.2 RAII 思想C RAII资源获取即初始化利用局部对象的生命周期自动管理资源。C11 标准库std::lock_guardstd::mutex栈对象构造时加锁析构时解锁。Qt 对应封装类QMutexLocker功能等价接收锁指针作为构造参数栈局部对象出作用域自动析构自动执行 unlock。1.3.3 QMutexLocker 代码示例#includethread.h#includeQMutexLockerintThread::num0;QMutex Thread::mutex;Thread::Thread(){}voidThread::run(){for(inti0;i50000;i){// 局部栈对象构造时lock(mutex)QMutexLockerlocker(mutex);num;// 无需手动unlock出循环迭代的大括号locker局部对象析构自动调用unlock}}无论内部 return、异常抛出只要离开当前作用域局部 locker 对象析构锁一定会释放。1.3.4 Qt 锁与 C 标准锁混用说明Qt 锁QMutex与 C 标准锁std::mutex底层都封装操作系统原生互斥 API技术上可以混用。工程实践不推荐混用接口、守卫类不统一QMutexLockervsstd::lock_guard提高维护成本容易写出错误代码。1.4 Qt 读写锁 QReadWriteLock1.4.1 适用场景少写多读业务场景大量线程并发读取共享数据极少线程修改数据。普通互斥锁 QMutex读写全部串行读操作之间也会互相阻塞并发性能差。读写锁特性读锁与读锁不互斥多个线程可以同时获取读锁并发读取读锁与写锁互斥写锁与写锁互斥写操作独占资源同一时刻只允许一个线程执行写。1.4.2 配套守卫类QReadLocker读锁 RAII 守卫构造上读锁析构释放读锁QWriteLocker写锁 RAII 守卫构造上写锁析构释放写锁1.4.3 读写锁代码示例#includeQReadWriteLockQReadWriteLock rwLock;// 读操作多线程可并发执行{QReadLockerlocker(rwLock);// 执行只读访问共享资源}// 作用域结束自动释放读锁// 写操作独占访问{QWriteLockerlocker(rwLock);// 修改共享资源}// 作用域结束自动释放写锁1.5 核心面试考点总结使用 QMutex 必须保证多线程操作同一个锁实例static 成员是常用实现方式。wait()作用主线程阻塞等待子线程执行完毕不 wait 直接读取共享变量会读到未计算完成的中间值。num不是原子操作是线程不安全典型案例。优先使用 RAII 守卫类QMutexLocker不要裸写 lock/unlock规避锁泄漏。QReadWriteLock 优势少读多写场景提升并发吞吐量写的时候所有读全部阻塞。锁泄漏锁加完之后没有释放会造成其他线程永久阻塞。Qt 锁与 C 标准锁底层都是操作系统 mutex 封装但工程禁止随意混用。结尾uu们本文的内容到这里就全部结束了艾莉丝在这里再次感谢您的阅读艾莉丝努力练剑C/C Linux 底层探索者 | 一个正在努力练剑的技术博主【关注】跟随我一起深耕技术领域见证每一次成长。❤️【点赞】让优质内容被更多人看见让知识传递更有力量。⭐【收藏】把核心知识点存好在需要时随时查、随时用。【评论】分享你的经验或疑问评论区一起交流避坑不要忘记给博主“一键四连”哦“今日练剑达成”“技术之路难免有困惑但同行的人会让前进更有方向。”结语希望对学习Linux相关内容的uu有所帮助不要忘记给博主“一键四连”哦往期回顾【QT】系统相关多线程QThread基础博主在这里放了一只小狗大家看完了摸摸小狗放松一下吧૮₍ ˶ ˊ ᴥ ˋ˶₎ა