QWaitCondition 是 Qt 中的条件变量类,用于在多线程环境下实现线程间的等待和唤醒机制。它提供了一种线程同步的方式,允许线程在满足特定条件之前等待,并在条件满足时被唤醒继续执行。
QWaitCondition常用方法:
// 线程将被阻塞,直到被唤醒。等待条件满足,并释放给定的互斥锁mutex。
void wait(QMutex *mutex);
// 线程将被阻塞,直到被唤醒。等待条件满足,并释放给定的读写锁lock。
void wait(QReadWriteLock *lock);
// 线程将被阻塞,直到被唤醒或超时。等待条件满足,最多等待time毫秒,并释放给定的互斥锁 mutex。
void wait(QMutex *mutex, unsigned long time);
// 唤醒一个等待该条件的线程。
void wakeOne();
// 唤醒所有等待该条件的线程。
void wakeAll();
这里假设主线程(非Qt的UI线程)调用Send()往通信口发送一个数据包,然后阻塞等待回包才继续往下执行。另一个线程(通信线程)不断从通信口中接收数据并解析成数据包,然后唤醒主线程。下面是最简单的方法:
// 主线程
Send(&packet);
mutex.lock();
condition.wait(&mutex);
if (m_receivedPacket)
{
HandlePacket(m_receivedPacket); // 另一线程传来回包
}
mutex.unlock();
// 通信线程
m_receivedPacket = ParsePacket(buffer); // 将接收的数据解析成包
condition.wakeAll();
通常情况下,上述代码能跑得很好。但在某些特殊情况下,可能会出现混乱,大大降低通信可靠性。
在主线程中,调用 Send(&packet) 发送后,假如通信线程立即收到回包,在主线程还来不及调用 wait() 的时候,已经先 wakeAll() 了,显然这次唤醒是无效的,但主线程继续调用 wait(),然后一直阻塞在那里,因为该回的包已经回了。经测试出现这种现象的概率还是挺大的,因为我们不敢保证主线程总会被优先调度。即使主线程已经调用了 wait(),也不能保证底层操作系统的 wait_block 系统调用先于 wake 系统调用,毕竟wait() 函数也是层层封装的。
QWaitCondition::wait() 在使用时必须传入一个上锁的 QMutex 对象。这是很有必要的。而上述示例一代码中,我们虽然用了 mutex,但只是为了形式上传入QMutex参数,让编译器能正常编译而已,事实上,没有其它任何线程再用到这个mutex。而 mutex 本来就是让多个线程能协调工作的,所以上述示例一主线程用的 mutex 是无效的。
QWaitCondition 通常与互斥锁(QMutex)一起使用,以实现更复杂的线程同步和通信模式,如生产者-消费者模式或线程间的消息传递。一个或多个线程在等待条件满足时调用 wait() 进入等待状态,同时释放相应的互斥锁,从而允许其他线程在满足条件时对其进行修改。当条件满足时,其他线程可以通过调用 wakeAll() 或 wakeOne() 唤醒等待的线程,从而继续执行。
根据 Qt 手册(见下节内容),wait() 函数必须传入一个已上锁的 mutex 对象,在 wait() 执行过程中,mutex一直保持上锁状态,直到调用操作系统的wait_block 在阻塞的一瞬间把 mutex 解锁(严格说来应该是原子操作,即系统能保证在真正执行阻塞等待指令时才解锁)。另一线程唤醒后,wait() 函数将在第一时间重新给 mutex 上锁(这种操作也是原子的),直到显示调用 mutex.unlock() 解锁。
在通信线程也用上 mutex 后,整个通信时序正常了,完全解决了示例一的问题。代码如下:
// 示例二
// 主线程
mutex.lock();
Send(&packet);
condition.wait(&mutex);
if (m_receivedPacket)
{
HandlePacket(m_receivedPacket); // 另一线程传来回包
}
mutex.unlock();
// 通信线程
m_receivedPacket = ParsePacket(buffer); // 将接收的数据解析成包
mutex.lock();
condition.wakeAll();
mutex.unlock();
上述示例二中,主线程先把 mutex 锁占据,即从发送数据包开始,一直到 QWaitCondition::wait() 在操作系统层次真正执行阻塞等待指令,这一段主线程的时间段内,mutex 一直被上锁,即使通信线程很快就接收到数据包,也不会直接调用 wakeAll(),而是在调用 mutex.lock() 时阻塞住(因为主线程已经把mutex占据上锁了,再尝试上锁就会被阻塞),直到主线程 QWaitCondition::wait() 真正执行操作系统的阻塞等待指令并释放mutex,通信线程的 mutex.lock() 才即出阻塞,继续往下执行,调用 wakeAll(),此时一定能唤醒主线程成功。
由此可见,通过 mutex 把有严格时序要求的代码保护起来,同时把 wakeAll() 也用同一个 mutex 保护起来,这样能保证:一定先有 wait() ,再有 wakeAll(),不管什么情况,都能保证这种先后关系,而不至于摆乌龙。
总结
QT没有给出底层的具体逻辑,我也没查到C++的std::condition_variable 原理的说明。结合上面示例二,我目前理解的全过程是:
主线程mutex.lock()获得锁,上锁;
主线程wait()成功后解锁,进入阻塞状态(OS保证wait_block阻塞的瞬间释放锁);
通信线程mutex.lock()获得锁,上锁;
通信线程调用condition.wakeAll(),OS的进程调度器尝试唤醒关联的所有线程,所以主线程进入唤醒状态,并尝试获得mutex锁。此时mutex已被通信线程上锁,所以主线程肯定无法获得mutex锁,进程调度器决定让主线程继续阻塞。注意,区别于之前的wait_block调用阻塞,这次阻塞是上锁阻塞)。
通信线程mutex.unlock()释放锁,进程调度器尝试唤醒这个锁关联的某一个线程;
主线程作为mutex唯一关联的线程,获得了锁(mutex.lock()),wait()终于返回;
主线程处理完的业务逻辑,调用mutex.lock()释放锁。
QWaitCondition文档:
QWaitCondition allows a thread to tell other threads that some sort of condition has been met. One or many threads can block waiting for a QWaitCondition to set a condition with wakeOne() or wakeAll(). Use wakeOne() to wake one randomly selected thread or wakeAll() to wake them all.
For example, let's suppose that we have three tasks that should be performed whenever the user presses a key. Each task could be split into a thread, each of which would have a run() body like this:
forever {
mutex.lock();
keyPressed.wait(&mutex);
do_something();
mutex.unlock();
}
Here, the keyPressed variable is a global variable of type QWaitCondition.
A fourth thread would read key presses and wake the other three threads up every time it receives one, like this:
forever {
getchar();
keyPressed.wakeAll();
}
The order in which the three threads are woken up is undefined. Also, if some of the threads are still in do_something() when the key is pressed, they won't be woken up (since they're not waiting on the condition variable) and so the task will not be performed for that key press. This issue can be solved using a counter and a QMutex to guard it. For example, here's the new code for the worker threads:
forever {
mutex.lock();
keyPressed.wait(&mutex);
++count;
mutex.unlock();
do_something();
mutex.lock();
--count;
mutex.unlock();
}
Here's the code for the fourth thread:
forever {
getchar();
mutex.lock();
// Sleep until there are no busy worker threads
while (count > 0) {
mutex.unlock();
sleep(1);
mutex.lock();
}
keyPressed.wakeAll();
mutex.unlock();
}
The mutex is necessary because the results of two threads attempting to change the value of the same variable simultaneously are unpredictable.
Wait conditions are a powerful thread synchronization primitive. The Wait Conditions Example example shows how to use QWaitCondition as an alternative to QSemaphore for controlling access to a circular buffer shared by a producer thread and a consumer thread.