线程同步

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

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

低级同步原语

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

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

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

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

注意: Qt 的同步类依赖于使用正确对齐的指针. 例如, 你不能在 MSVC 中使用打包类.

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

风险

如果一个线程锁定了一个资源, 却在没有使用完毕后解锁它, 则可能会导致程序冻结, 因为其他线程将永远无法访问该资源. 这很容易出现在, 如, 有异常抛出, 并强制当前函数返回, 而没有释放锁的场景中.

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

便捷类

QMutexLocker, QReadLockerQWriteLocker 使得 QMutexQReadWriteLock 使用起来更加简单. 这些类会在它们构造时锁定资源, 在析构时自动解锁资源. 设计它们的初衷是简化使用 QMutexQReadWriteLock 的代码, 降低资源被意外永久锁定的可能性.

高级事件队列

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

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

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

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

另见 QThread::exec(), Threads and QObjects.