同步线程

尽管线程的初衷是让代码并行运行,仍然有许多时候,线程必须停下来等待其他线程。例如,若有两个线程尝试同时写入同一个变量,则结果是不确定的。让线程强制等待另外线程,这样的机制被称为互斥,是一种保护共享资源(如数据)的常用技术。

Qt 在为线程同步提供高级机制的同时,也提供了低级原语。

低级同步原语

QMutex 是能够进行强制互斥的基础类。为了获取对一个共享资源的访问权,线程会锁住一个互斥锁。若此时,另一个线程尝试再次加锁,则导致后者进入睡眠状态,直到第一个线程完成任务并解开互斥锁。

QReadWriteLock 类似 QMutex ,只是前者区分了“读”与“写”的访问权限。当一段数据未处于写入过程中时,多个线程可以安全地同时读取它。 QMutex 能强制多个线程轮流访问共享数据,而 QReadWriteLock 允许同时读取,提高了并行性。

QSemaphoreQMutex 的泛化,它保护一定数量的同类资源。QMutex 则与之相反,仅保护一个资源。信号量示例QSemaphore 的一个使用典例:同步访问在生产者和使用者之间的循环缓冲区。

QWaitCondition 不同于强制互斥,它提供一个 条件变量 来同步线程。不同于其他原语令线程等待至资源解锁,QWaitCondition 可以让线程在符合特定条件时退出等待。为了让等待中的线程继续执行,调用 wakeOne() 来随机唤醒一个线程,或者调用 wakeAll() 来同时唤醒所有线程。等待条件示例 将告诉您如何使用 QWaitCondition 取代 QSemaphore 来解决生产者-消费者问题。

注意:Qt 的同步类依赖于使用正确对齐的指针。例如,您不能在 MSVC 中使用打包类。(译者注:即使用 #pragma pack 或 alignas 等方式非常规对齐地存储同步类对象)

这些线程同步类能让一个方法做到线程安全。不过,这会导致性能损失,因此大多数 Qt 的方法都不保障线程安全。


风险

如果一个线程锁定了一个资源,却在没有使用完毕后解锁它,则可能会导致程序冻结,因为其他线程将永远无法访问该资源。这很容易出现在,例如,有异常抛出,并强制当前函数返回,而没有释放锁的场景中。(译者注:异常抛出时,当前函数会被强制就地返回,本地栈变量会被释放。因此在引发异常的代码之后的 unlock() 不会被执行)

另一个类似的场景是 死锁 。例如,假设线程 A 正在等待线程 B 解锁一个资源。若此时线程 B 也在等待线程 A 解锁另一个资源,那么两个线程持续相互等待,这将造成程序冻结。


提供便利的类

QMutexLockerQReadLockerQWriteLocker 使得 QMutexQReadWriteLock 使用起来更加简单。这些类会在它们构造时锁定资源,在析构时自动解锁资源。设计它们的初衷是简化使用 QMutexQReadWriteLock 的代码,降低资源被意外永久锁定的可能性。(译者注:此方式可以保证线程安全,即在抛出异常,导致函数强制返回时,锁会因为本地的 locker 栈对象析构而被自动解锁)

高级事件队列

Qt 的 事件系统 在跨线程通信中非常有用。每个线程都有自己的事件循环。要令槽函数(或任何可动态调用的方法)在另一个线程中被调用,可将该调用目标置入目标线程的事件循环中。这可让目标线程完成当前任务后再执行槽函数,而原始线程继续并行运行。

若要将调用代码置于事件循环中,可创建一个队列 信号槽 连接。每当发出信号时,事件系统将记录它的参数。该信号接收者 所属 的线程将会执行对应的槽函数。此外,也可以使用 QMetaObject::invokeMethod() 以达到同样效果,同时无需发射信号。在这两种情况下,都必须使用 队列连接,因为 直接连接 会绕过事件系统,在当前线程中立刻执行该方法。

不同于使用低级原语,使用事件系统进行线程同步没有死锁的风险。不过,事件系统不会强制执行互斥。如果可动态调用的方法访问共享数据,则依然需要使用低级原语对其进行保护。

即便如此,Qt 的事件系统以及 隐式共享 的数据结构依然提供了传统线程锁定的替代方案。如果仅使用信号槽,而且线程间无共享变量,多线程程序完全可以不使用低级原语。

另请参阅QThread::exec() 以及 线程与 QObject

< Qt 中的多线程技术 可重入性与线程安全 >