【QT 之 Synchronizing Threads】 同步线程
虽然线程的目的是允许代码并行运行,但有时线程必须停止并等待其他线程。例如,如果两个线程试图同时写入同一个变量,结果是未定义的。强制线程相互等待的原理称为互斥。这是一种用于保护共享资源(如数据)的通用技术。
Qt提供了用于同步线程的低级原语以及高级机制。
Low-Level Synchronization Primitives(低级同步基元)
QMutex
是一个强制互斥的基础类,线程锁定互斥对象以获得对共享资源的访问权限。如果第二个线程试图在互斥锁已被锁定的情况下锁定它,那么第二个进程将进入睡眠状态,直到第一个线程完成任务并解锁互斥锁。
QReadWriteLock
类似于QMutex
,只是它区分了“读”和“写”访问。当一段数据没有被写入时,多个线程同时从中读取是安全的。QMutex
强制多个读取器轮流读取共享数据,但QReadWriteLock
允许同时读取,从而提高了并行性。
QSemaphore
是QMutex
的推广,它保护一定数量的相同资源。相反,QMutex
只保护一个资源。
QWaitCondition
不是通过强制互斥而是通过提供条件变量
来同步线程。当其他原语使线程等待资源解锁时,QWaitCondition
使线程等待,直到满足特定条件。要允许等待的线程继续,请调用wakeOne()
来唤醒一个随机选择的线程,或者调用wakeAll()
来同时唤醒所有线程。
这些同步类可以用于使方法线程安全。然而,这样做会导致性能损失,这就是为什么大多数Qt方法不能实现线程安全的原因。
Risks
如果线程锁定了资源但没有解锁,则应用程序可能会冻结,因为其他线程将永远无法使用该资源。例如,如果抛出异常并强制当前函数返回而不释放其锁,就会发生这种情况。
另一种类似的情况是死锁。例如,假设线程A正在等待线程B解锁资源。如果线程B也在等待线程A解锁不同的资源,那么两个线程最终都将永远等待,因此应用程序将冻结。
方便的类
QMutexLocker、QReadLocker和QWriteLocker
是使QMutex和QReadWriteLock
更易于使用的便利类。它们在构建资源时锁定资源,在销毁资源时自动解锁资源。它们旨在简化使用QMutex和QReadWriteLock
的代码,从而减少资源意外被永久锁定的可能性。
高级事件队列
Qt的事件系统
对于线程间通信非常有用。每个线程都可能有自己的事件循环。要调用另一个线程中的槽(或任何可调用的方法),请将该调用放在目标线程的事件循环中。这允许目标线程在插槽开始运行之前完成其当前任务,而原始线程继续并行运行。
要在事件循环中放置调用,请进行排队的信号槽连接。无论何时发出信号,事件系统都会记录其自变量。然后,信号接收器所在的线程将运行该插槽。或者,调用QMetaObject::invokeMethod()
以在没有信号的情况下实现相同的效果。在这两种情况下,都必须使用排队连接,因为直接连接会绕过事件系统并立即在当前线程中运行该方法。
与使用低级原语不同,使用事件系统进行线程同步时没有死锁的风险。但是,事件系统并不强制执行互斥。如果可调用方法访问共享数据,则它们仍然必须使用低级原语进行保护。
话虽如此,Qt的事件系统以及隐式共享的数据结构提供了传统线程锁定的替代方案。如果只使用信号和槽,并且线程之间不共享任何变量,那么多线程程序可以完全不使用低级原语。