线程同步
虽然线程的目的是允许代码并行运行,但有时线程需要停止并等待其他线程。例如,如果两个线程同时尝试写入同一变量,结果将是未定义的。强制线程相互等待的原则被称为互斥。这是一种常见的保护共享资源(如数据)的技术。
Qt 提供了低级原语以及高级机制来同步线程。
低级同步原语
QMutex 是强制互斥的基本类。一个线程锁住互斥量以便访问共享资源。如果第二个线程在互斥量已锁定的同时尝试锁定它,则第二个线程将被置于休眠状态,直到第一个线程完成任务并解锁互斥量。
QReadWriteLock 与 QMutex 类似,但它区分了“读取”和“写入”访问。当一项数据未被编写时,多个线程同时读取它是安全的。一个 QMutex 迫使多个读者轮流读取共享数据,而一个 QReadWriteLock 允许多个线程同时读取,从而提高了并行性。
QSemaphore 是 QMutex 的一般化,它保护一定数量的相同资源。相比之下,一个 QMutex 只保护一个资源。《使用信号量的生产者和消费者》示例展示了信号量的典型应用:同步生产者和消费者之间对循环缓冲区的访问。
QWaitCondition 不是通过强制互斥来同步线程,而是通过提供一个条件变量。其他原语使线程等待直到资源被解锁,而 QWaitCondition 使线程等待直到满足特定条件。要允许等待的线程继续执行,请调用 wakeOne() 以唤醒随机选择的线程或 wakeAll() 以同时唤醒所有线程。《使用等待条件的生产者和消费者》示例展示了如何使用 QWaitCondition 而不是 QSemaphore 来解决生产者-消费者问题。
注意:Qt 的同步类依赖于正确对齐的指针的使用。例如,你不能使用 MSVC 中的打包类。
这些同步类可用于使方法线程安全。但是,这样做会带来性能损失,这也是为什么大多数 Qt 方法都不是线程安全的原因。
风险
如果一个线程锁定了一个资源但没有解锁它,应用可能因为资源永久不可用给其他线程而导致冻结。例如,如果在抛出异常时强制当前函数返回而不释放其锁,就可能会发生这种情况。
另一个类似的场景是死锁。例如,假设线程A正在等待线程B解锁一个资源。如果线程B也在等待线程A解锁另一个资源,那么这两个线程都将陷入永久的等待中,从而导致应用冻结。
便利类
QMutexLocker、QReadLocker和QWriteLocker是一些便利类,它们使使用QMutex和QReadWriteLock变得更加容易。它们在构造时锁定资源,在被销毁时自动解锁。它们旨在简化使用QMutex和QReadWriteLock的代码,从而减少因意外而导致资源被永久锁定的可能性。
高级事件队列
Qt的事件系统非常有用,可用于线程间通信。每个线程可能都有自己的事件循环。要在另一个线程中调用槽(或任何可调用方法),将调用放在目标线程的事件循环中。这将在槽开始运行之前让目标线程完成其当前任务,并且原始线程可以并行继续运行。
要将调用放入事件循环中,请创建队列信号-槽连接。每当信号被触发时,事件系统都会记录其参数。然后信号接收器所在的线程将运行槽。或者,使用QMetaObject::invokeMethod()来达到相同的效果,而不使用信号。在这两种情况下,都必须使用队列连接,因为直接连接绕过了事件系统,并在当前线程中立即运行方法。
与使用低级原语相比,在使用事件系统进行线程同步时不存在死锁风险。但是,事件系统不强制实现互斥。如果可调用方法访问共享数据,它们仍然需要用低级原语进行保护。
话虽如此,Qt的事件系统以及隐式共享数据结构为传统的线程锁定提供了一种替代方案。如果只使用信号和槽,并且线程之间不共享任何变量,那么多线程程序可以完全不使用低级原语。
另请参阅QThread::exec() 和 线程和 QObjects。
© 2024 Qt公司有限公司。 本文档贡献的版权属于其各自的所有者。本提供的文档根据自由软件基金会发布的GNU自由文档许可协议版本1.3的条款进行许可。Qt和相应的标志是芬兰和/或其他国家的Qt公司有限责任公司的商标。所有其他商标均为其各自所有者的财产。