在使用多线程时需要时刻注意一点的时,多个线程在访问同一个资源时会抢夺资源,造成数据不一致,严重影响程序结果甚至崩溃。为了防止竞态条件的发生,使用多线程时需要实现线程同步,也即确保多个线程在同时使用共享资源时不会发生冲突或数据不一致。
Qt提供了互斥锁、信号量、条件变量、读写锁以及原子操作等机制实现线程同步。
文章目录
- 互斥锁
- 读写锁
- 条件变量
- 信号量
互斥锁
互斥锁QMutex是最常用的一种同步机制,用于保护一个对象或数据结构或一段代码,确保同一时间只有一个线程访问它们。
| 方法 | 说明 |
|---|---|
| lock() | 对互斥资源加锁,如果有其他线程拥有当前锁,调用此函数会阻塞直到其他线程释放了锁,通常与unlock()成套使用。注意不要对同一个互斥量多次加锁,否则会造成死锁 |
| tryLock(int timeout) | 尝试获取互斥锁,如果获取成功返回true,否则返回false,如果另一个线程已经锁定了该互斥量,此函数将等待直到达到设定的超时时间(ms),如果成功获得了锁,则必须在其他线程能够成功锁定该互斥量之前,调用unlock()将其解锁。从同一线程中对同一个互斥量多次调用此函数将导致死锁。 |
| try_lock() | 等同于不带参数的tryLock(),是为了与标准库兼容而提供的 |
| unlock() | 释放当前持有的锁,需与lock()成对使用。试在与锁定该互斥量不同的线程中对其进行解锁会导致错误。对一个未被锁定的互斥量执行解锁操作将导致未定义行为 |
QMutex在使用时需要手动锁定和解锁。在简单的程序中尚可,在复杂程序中很容易忽略细节而出现问题,我们推荐QMutexLocker配合QMutex一起使用。QMutexLocker是利用RAII机制,在构造函数中加锁,析构函数中解锁,因此不需要我们手动加锁和解锁,使用更方便。为此QMutexLocker对象通常创建为局部变量,并传入一个未加锁的QMutex指针变量(若是锁定,可用relock()重新锁定或unlock())。
QMutex lock; void TestThread::run() { ... lock.lock(); dosomething(); lock.unlock(); ... } void TestThread::run() { ... QMutexLocker(&lock); dosomething(); ... }有些场景如函数递归或重入,注意要避免重复加锁而造成死锁:
QMutex mutex; void increment() { QMutexLocker locker(&mutex); ++count; } void incrementTwice() { QMutexLocker locker(&mutex); // 第一次获取锁 increment(); // ❌ 尝试再次获取同一个锁 → 死锁! }对于这种情况需要用到递归锁:
QRecursiveMutex mutex; void increment() { QMutexLocker locker(&mutex); ++count; } void incrementTwice() { QMutexLocker locker(&mutex); //第一次获取锁 increment(); // 第二次获取同一个锁,递归 } //析构时释放2次锁递归锁需要记录持有者和次数,性能略低于普通锁,而且可能隐藏设计问题,因优先优化代码架构。
读写锁
QReadWriteLock读写锁允许多个线程同时以只读方式访问资源,但一旦有某个线程需要写入资源,所有其他线程(无论是读还是写)都必须被阻塞,直到写操作完成。在许多情况下,QReadWriteLock是QMutex的直接替代方案。如果应用程序中存在大量并发读取操作,而写入操作相对较少,则使用QReadWriteLock是一个很好的选择。QReadWriteLock正常情况下多个读者可以同时读(并发),一旦有写者在排队等候(即使没有开始写),系统也会阻止新读者加入,已经在读的读者可以继续读完,但新来的读者必须等待写者完成,这样设计的目的是防止读者源源不断的到来,写者永远等不到读者的时刻,写者被饿死。当有一个写者W1持有写锁,此时读者R1、R2在排队等候,如果有新的写者W2到来,即使R1、R2等得更久,W2也会优先于R1、R2获得锁,也即写者优先。
| 特性 | QMutex | QReadWriteLock |
|---|---|---|
| 锁类型 | 互斥锁 | 读写锁 |
| 并发读 | 不支持 | 允许多个线程同时读 |
| 写操作 | 与其他任何操作互斥 | 写时禁止所有其他读写 |
| 使用场景 | 读写频率相近,或写多读少 | 读多写少 |
QReadWriteLock类常用的方法包括lockForRead()、lockForWrite()以及unlock(),lockForRead()和lockForWrite()需要和unlock()配套使用。
QReadWriteLock lock; void ReaderThread::run() { ... lock.lockForRead(); read_file(); lock.unlock(); ... } void WriterThread::run() { ... lock.lockForWrite(); write_file(); lock.unlock(); ... }为了防止unlock()忘记调用,推荐使用QWriteLocker和QReadLocker,这是利用了RAII机制,在构造的时候加锁,析构的自动解锁。
上述代码等效于:
void ReaderThread::run() { ... QReadLocker locker(&lock); read_file(); ... } void WriterThread::run() { ... QWriteLocker locker(&lock); write_file(); ... }条件变量
QWaitCondition是Qt条件变量的实现,它通常配合互斥锁(QMutex)或读写锁(QReadWriteLock)使用,用来让线程在某个条件为真之前阻塞等待,由其他线程在条件改变后调用 wakeOne()/wakeAll() 唤醒。
条件变量和QMutex不同,QMutex要解决的问题是互斥,要保证同一时间只有一个线程进入,防止数据竞争,而QWaitCondition是要解决的是条件等待以及唤醒线程的问题,它能够高效的利用资源,避免无意义的轮询,充分利用CPU资源。
以生产者线程和消费者模型为例,如果只用QMutex同步线程,消费者线程需要不断地轮询检查:获取锁–>检查队列–>还为空–>睡眠–>再次获取锁…
这样如果队列为空CPU就会陷入忙等,白白浪费CPU资源,如果队列不为空,其他线程可能需要等到下一个轮询点才发现队列不为空,反应迟钝。