翻译自Qt帮助文档:Synchronizing Threads
多线程允许并行运行,有时候线程必须停下来等待其他的线程。例如,如果两个线程尝试同时往一个变量中写值,结果是未知的。强制线程等待另一个线程的规则叫互斥(mutal exclusion)。这种个普通的技术用来保护例如数据这样的共享资源。
对于同步线程,Qt提供了低级的基元和高级机制。
低级同步基元
QMutex 是强制互斥的基类。为了访问一个共享资源,一个线程锁住一个mutex。当mutex已经被锁住时,如果第二个线程尝试锁住它,那么第二个线程将会进入睡眠状态,直到第一个线程完成任务并解锁mutex。
除了区别对待read 和 write操作,QReadWriteLock 和QMutex其实是类似的。一段数据没被写入时,多线程同步读取数据是安全的。一个QMutex 强制多个读者按顺序读取共享数据,但是一个QReadWriteLock 允许同时读操作,从而提高并行性。
QSemaphore 是 QMutex 的泛化,它保护一定数量的相同资源。相反的,QMutex只保护一个资源。
QWaitCondition 不是通过强制互斥而是通过提供一个condition值达到同步线程的。当其他的基元让线程等待直到资源被解锁,QWaitCondition 会让线程等待直到满足一个特定的条件时。为了让等待的线程继续,调用 wakeOne() 来唤醒一个随机选择的线程或者调用 wakeAll() 来同时唤醒他们。
注意:这些同步类依赖于对合理的对齐的指针的使用。例如,你可以使用MSVC类包。
这些同步类可以用来让一个线程变的安全的。然而,这样做会导致性能有所下降,这是为什么大多数Qt方法并不是线程安全的原因。
风险
如果一个线程锁着一个资源但没有解锁,应用程序可能会阻塞,因为对于其他的线程这个资源将会永远不可用。这种情况完全有可能会发生,例如,如果一个被抛出来的异常,它强制要求当前函数返回,但未释放它的锁。
另一个相似的情形就是死锁(deadlock)。例如,假如线程A等待线程B去解锁一个资源。如果线程B同样在等待线程A去解锁一个不同的资源,那么这两个线程将会永远在等待,因此应用程序将会阻塞。
方便的类
QMutexLocker,QReadLocker 和 QWriteLocker 就是方便的类,它们可以让我们很方拜年的使用 QMutex 和 QReadWriteLock 类。当它们被创建的时候,它们就锁住一个资源,当它们被销毁时自动解锁资源。它们被设计用来简化那些使用了QMutex 和 QReadWriteLock 的代码,从而可以减少资源被永久锁住的几率。
高级事件队列
Qt的事件系统对线程间的交互非常有用。每个线程可以有自己的事件循环。为了调用其他线程的一个槽(或者任何可调用的方法),将那个调用放在目标线程的事件循环中。这让目标线程在槽开始运行前完成它自己当前的任务,期间原始线程继续并行运行。
为了将一个调用放在事件循环中,做一个队列话的信号-槽连接。当信号被发送时,它的实参将会被事件系统记录下来。接收信号的线程将会运行对应的槽。另外,不是用信号时调用QMetaObject::invokeMethod() 可以获取相同的效果。在这两种情况下,必须使用一个队列话的连接(queued connection),因为直接连接(direct connection)会绕过时间系统,并在当前线程中立即运行方法。
当为线程同步使用事件系统时,发生死锁是没有风险的,不像使用低级基元。然而,事件系统不会强制互斥。如果调用的方法访问共享的数据,它们必须使用低级基元来保护操作。
已经说过,伴随着隐式共享数据结构,Qt的事件系统提供了一个可替代传统线程锁定的方法。如果只使用信号和槽,并且两个线程间并没有共享的数据,多线程程序在没有低级基元的情况下完全可以做到。
注释:queued connection:当控制返回到接受者线程的事件循环中时,槽才会被调用。此时槽是在接收者的线程中执行的。
direct connection:当发送信号时,槽被立即执行,并且槽是在发送信号的线程中执行的。