QWaitConditon也是用来同步线程的。从名字来看是等待条件,意思就是线程阻塞在等待条件的地方,直到条件满足才继续执行下去。等待条件的线程可以是一个或者多个。用QWaitCondition的函数表示过程如下:
1.等待条件的线程调用QWaitCondition::wait()阻塞。
2.实现条件的线程通过计算完成条件后调用QWaitConditon::wakeOne()或者QWaitCondition::wakeAll()。
3.当2中调用wake之后,继续执行wait之后的操作。
其中wakeOne会随机唤醒等待的线程中的一个。wakeAll会唤醒所有的等待线程。
写个例子测试一下:
1.创建两个线程类Thread和ThreadTwo, Thread用来等待,ThreadTwo来唤醒。两个类的run函数分别如下:
Thread的run函数功能就是等待被唤醒,然后输出一句话。ThreadTwo的run函数就是每隔2秒唤醒一次。
2.创建2个Thread类的对象,和1个ThreadTwo的对象,并调用start。然后查看执行情况。
程序运行后会按上图中箭头所划分,每两秒输出一部分。也就是说ThreadTwo每两秒唤醒了一个Thread的线程。如果ThreadTwo调用的是wakeAll函数那么在输出0时两个线程都被唤醒,运行结果如下:
说明:由于ThreadTwo的run函数中先调用了sleep确保了wake会在Thread的wait之后发生,因此没有使用QMutex来保证他们的执行顺序。
以下讨论QMutex与QWaitConditon结合使用的情况:
简单用法
QWaitCondition 用于多线程的同步,一个线程调用QWaitCondition::wait() 阻塞等待,直到另一个线程调用QWaitCondition::wake() 唤醒才继续往下执行。
为了描述方便,这里假设主线程调用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 是无效的。
根据 Qt 手册,wait() 函数必须传入一个已上锁的 mutex 对象,在 wait() 执行过程中,mutex一直保持上锁状态,直到调用操作系统的wait_block 在阻塞的一瞬间把 mutex 解锁(严格说来应该是原子操作,即系统能保证在真正执行阻塞等待指令时才解锁)。另一线程唤醒后,wait() 函数将在第一时间重新给 mutex 上锁(这种操作也是原子的),直到显式调用 mutex.unlock() 解锁(指的也是wait() 函数所在线程执行的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(),不管什么情况,都能保证这种先后关系,而不至于摆乌龙。
推而广之
mutex 和 condition 联合使用是多线程中的一个常用的设计模式,不仅是 Qt,对于 C++ 的 std::condition_variable 和 std::mutex ,以及 java 的 synchronized / wait / notify 也都适用。
转自:https://www.fearlazy.com/index.php/post/101.html
https://blog.csdn.net/flyoxs/article/details/54617342