3. 线程同步基础:QMutex基础用法、QMutexLocker自动解锁、死锁的产生与预防、线程安全队列实现
多线程编程里,最让人头疼的问题是什么?我个人觉得,不是线程创建,也不是任务调度,而是——数据打架。
两个线程同时往一个变量里写数据,结果谁也不知道最终值是多少。这就是典型的竞态条件。解决它的核心手段,就是线程同步。今天咱们就从最基础的 QMutex 开始,一步步把同步这件事聊透。
3.1 QMutex 基础用法
QMutex 是 Qt 提供的最基本的互斥锁。它的作用很简单:同一时刻,只允许一个线程访问被保护的资源。
用法其实就三个步骤:
- 定义一个 QMutex 对象
- 在访问共享资源前调用
lock() - 访问结束后调用
unlock()
看个例子:
// 全局变量
QMutex mutex;
int sharedCounter = 0;
void workerThread() {
for (int i = 0; i < 1000; ++i) {
mutex.lock();
++sharedCounter; // 临界区
mutex.unlock();
}
}
嗯,这里要注意:lock() 和 unlock() 必须成对出现。少一个,另一个线程就永远进不去。多一个,程序直接崩溃。
unlock() 就不会被执行。锁就永远解不开了。这就是为什么我们不推荐裸用 lock/unlock。
3.2 QMutexLocker 自动解锁
为了解决上面那个问题,Qt 提供了 QMutexLocker。它是个 RAII 封装类——构造时自动 lock,析构时自动 unlock。
说白了,你只管在临界区开头声明一个 QMutexLocker,剩下的交给它。哪怕中间 return 了、抛异常了,锁也会被正确释放。
void safeWorker() {
QMutexLocker locker(&mutex); // 自动 lock
++sharedCounter;
// 函数结束时,locker 析构,自动 unlock
}
我个人习惯是:只要用到 QMutex,就一定配合 QMutexLocker。裸写 lock/unlock 的代码,我基本不写。为什么?因为我在项目中吃过亏——有一次临界区里调了一个第三方函数,那个函数内部抛了异常,结果锁没释放,整个界面卡死了。从那以后,我就只用 QMutexLocker。
relock() 和 unlock() 方法,可以在需要时手动控制锁的粒度。但大多数场景下,让它自动管理就够了。
3.3 死锁的产生与预防
死锁,说白了就是两个线程互相等对方释放锁。你拿着锁 A 等锁 B,我拿着锁 B 等锁 A,谁都不让,程序就卡死了。
举个例子:
QMutex mutexA, mutexB;
void thread1() {
QMutexLocker lockerA(&mutexA);
// ... 做一些事
QMutexLocker lockerB(&mutexB); // 等 mutexB
}
void thread2() {
QMutexLocker lockerB(&mutexB);
// ... 做一些事
QMutexLocker lockerA(&mutexA); // 等 mutexA
}
如果 thread1 拿到了 mutexA,thread2 拿到了 mutexB,那两边就永远卡住了。
怎么预防?我总结了三条经验:
- 固定加锁顺序:所有线程都按同样的顺序加锁。比如先 A 后 B,就不会出现循环等待。
- 使用 tryLock:
tryLock()不会阻塞,如果拿不到锁就返回 false。可以避免死等。 - 缩小锁范围:锁的粒度越小,死锁概率越低。别把整个函数都锁住。
3.4 线程安全队列实现
讲完了基础,咱们来点实战。实现一个线程安全的队列,这是多线程编程里最常见的需求之一。
思路很简单:用 QMutex 保护队列的 push 和 pop 操作。但要注意,pop 时如果队列为空,不能让线程空转,得让它等待。这时候就需要 QWaitCondition 了。
#include <QQueue>
#include <QMutex>
#include <QWaitCondition>
template<typename T>
class ThreadSafeQueue {
public:
void push(const T &value) {
QMutexLocker locker(&mutex);
queue.enqueue(value);
cond.wakeOne(); // 唤醒一个等待的线程
}
T pop() {
QMutexLocker locker(&mutex);
while (queue.isEmpty()) {
cond.wait(&mutex); // 等待,并释放锁
}
return queue.dequeue();
}
private:
QQueue<T> queue;
QMutex mutex;
QWaitCondition cond;
};
这里有个细节:cond.wait(&mutex) 会自动释放锁,让其他线程能 push 数据。被唤醒后,它会重新获取锁,然后继续执行。这个机制非常巧妙,避免了忙等待。
我曾经在一个音频采集项目里用过这个队列。主线程从麦克风读数据,push 到队列;工作线程从队列 pop 数据做处理。运行了几个月,没出过一次竞态问题。嗯,这就是同步的魅力。
cond.wait(&mutex, timeout)。返回 true 表示被唤醒,false 表示超时。这在 UI 线程中特别有用,可以避免界面卡死。
3.5 本章知识体系
下面这张图,帮你把本章的核心逻辑串起来:
从图中可以看到,线程同步的核心就是锁 + 条件变量。QMutex 负责互斥,QWaitCondition 负责等待与唤醒。两者结合,就能写出高效且安全的并发代码。
好了,这一章就到这里。记住一句话:锁是工具,不是目的。我们的目标不是用锁,而是让数据在并发环境下安全、高效地流动。
- QMutex 提供基本的互斥访问,但必须配合 QMutexLocker 使用
- QMutexLocker 是 RAII 封装,自动管理锁的获取与释放
- 死锁的预防核心是统一加锁顺序
- 线程安全队列 = QMutex + QWaitCondition,实现高效的生产者-消费者模式