线程同步
尽管线程的初衷是让代码并行运行, 仍然有许多时候, 线程必须停下来等待其他线程. 例如, 若有两个线程尝试同时写入同一个变量, 则结果是不确定的. 让线程强制等待另外线程, 这样的机制被称为 互斥 , 是一种保护共享资源 (如数据) 的常用技术.
Qt 在为线程同步提供高级机制的同时, 也提供了低级原语.
低级同步原语
QMutex 是能够进行强制互斥的基础类. 为了获取对一个共享资源的访问权, 线程会锁住一个互斥锁. 若此时, 另一个线程尝试再次加锁, 则导致后者进入睡眠状态, 直到第一个线程完成任务并解开互斥锁.
QReadWriteLock 与 QMutex 类似, 只是前者区分了 "read" 和 "write" 的访问权限. 当一段数据未处于写入过程中时, 多个线程可以安全地同时读取它. QMutex 能强制多个线程轮流访问共享数据, 而 a QReadWriteLock 允许同时读取, 提高了并行性.
QSemaphore 是 QMutex 的泛化, 它保护一定数量的同类资源. QMutex 则与之相反, 仅保护一个资源. 信号量示例 是 QSemaphore 的一个使用典例: 同步访问在生产者和使用者之间的循环缓冲区.
QWaitCondition 不同于强制互斥, 它提供一个 条件变量 同步线程. 不同于其他原语令线程等待至资源解锁, QWaitCondition 可以让线程在符合特定条件时退出等待. 为了让等待中的线程继续执行, 调用 wakeOne() 随机唤醒一个线程, 或者调用 wakeAll() 同时唤醒所有线程. 等待条件示例 告诉你如何使用 QWaitCondition 取代 QSemaphore 解决生产者-消费者问题.
注意: Qt 的同步类依赖于使用正确对齐的指针. 例如, 你不能在 MSVC 中使用打包类.
这些线程同步类能让一个方法做到线程安全. 不过, 这会导致性能损失, 因此大多数 Qt 的方法都不保障线程安全.
风险
如果一个线程锁定了一个资源, 却在没有使用完毕后解锁它, 则可能会导致程序冻结, 因为其他线程将永远无法访问该资源. 这很容易出现在, 如, 有异常抛出, 并强制当前函数返回, 而没有释放锁的场景中.
另一个类似的场景是 死锁. 例如, 假设线程 A 正在等待线程 B 解锁一个资源. 若此时线程 B 也在等待线程 A 解锁另一个资源, 那么两个线程持续相互等待, 这将造成程序冻结.
便捷类
QMutexLocker, QReadLocker 和 QWriteLocker 使得 QMutex 与 QReadWriteLock 使用起来更加简单. 这些类会在它们构造时锁定资源, 在析构时自动解锁资源. 设计它们的初衷是简化使用 QMutex 和 QReadWriteLock 的代码, 降低资源被意外永久锁定的可能性.
高级事件队列
Qt 的 事件系统 在跨线程通信中非常有用. 每个线程都有自己的事件循环. 要使槽函数 (或任何 可动态调用的 方法) 在另一个线程中被调用, 可将该调用目标置入目标线程的事件循环中. 这可让目标线程完成当前任务后再执行槽函数, 而原始线程继续并行运行.
若要将调用代码置于事件循环中, 可创建一个队列 信号槽 连接. 每当发出信号时, 事件系统将记录它的参数. 该信号接收者 所属 的线程将会执行对应的槽函数. 此外, 也可以使用 QMetaObject::invokeMethod() 以达到同样效果, 同时无需发射信号. 在这两种情况下, 都必须使用 队列连接, 因为 直接连接 会绕过事件系统, 在当前线程中立刻执行该方法.
不同于使用低级原语, 使用事件系统进行线程同步没有死锁的风险. 不过, 事件系统不会强制执行互斥. 如果可动态调用的方法访问共享数据, 则依然需要使用低级原语对其进行保护.
即便如此, Qt 的事件系统以及 隐式共享 的数据结构依然提供了传统线程锁定的替代方案. 如果仅使用信号槽, 而且线程间无共享变量, 多线程程序完全可以不使用低级原语.