C++ 中的并发详细介绍:条件变量

作者:微信公众号:【架构师老卢】
4-29 8:21
30

概述:过去,我们使用的无限轮询循环没有得到最佳编程:只要程序在运行,while-loop 就会让处理器保持忙碌,不断询问是否有新数据可用。在下文中,我们将研究一种更好的方法来解决这个问题,而不会给处理器带来太大的负载。轮询循环的替代方法是让主线程阻塞并等待新数据可用的信号。这将防止无限循环使处理器保持繁忙状态。我们已经讨论过一种能够实现这一目的的机制——承诺-未来结构。期货的问题在于它们只能使用一次。一旦未来准备好并被调用,它就不能再使用了。出于我们的目的,我们需要一种可以重复使用的信号机制。C++标准以“条件变量”的形式提供了这样的构造。get()A 有一个方法,当线程调用它时,它会阻塞。条件变量

过去,我们使用的无限轮询循环没有得到最佳编程:只要程序在运行,while-loop 就会让处理器保持忙碌,不断询问是否有新数据可用。在下文中,我们将研究一种更好的方法来解决这个问题,而不会给处理器带来太大的负载。

轮询循环的替代方法是让主线程阻塞并等待新数据可用的信号。这将防止无限循环使处理器保持繁忙状态。我们已经讨论过一种能够实现这一目的的机制——承诺-未来结构。期货的问题在于它们只能使用一次。一旦未来准备好并被调用,它就不能再使用了。出于我们的目的,我们需要一种可以重复使用的信号机制。C++标准以“条件变量”的形式提供了这样的构造。get()

A 有一个方法,当线程调用它时,它会阻塞。条件变量一直被阻止,直到它被另一个线程释放。该版本通过方法或方法工作。这两种方法之间的主要区别在于,notify_one只会唤醒一个等待的线程,而会同时唤醒所有等待的线程。std::condition_variablewait()notify_one()notify_allnotify_all

条件变量是更高级通信协议的低级构建块。它既没有自己的记忆,也不记得通知。假设一个线程在另一个线程调用之前调用,条件变量按预期工作,第一个线程将唤醒。想象一下,如果呼叫顺序被颠倒,那么在之前调用时,通知将丢失,线程将无限期阻塞。因此,在更复杂的通信协议中,条件变量应始终与另一个可以独立检查的共享状态结合使用。在这种情况下,通知条件变量仅意味着继续并检查其他共享状态。wait()notify()notify()wait()

让我们假设我们的共享变量是一个名为 的布尔变量。现在,让我们讨论协议的两种方案,这取决于谁先行动,生产者线程或消费者线程。dataIsAvailable

场景 1:

使用者线程进行检查,由于它是 false,因此使用者线程会阻塞并等待条件变量。稍后,生产者线程将 dataIsAvailable 设置为 true,并调用条件变量。此时,消费者醒来并继续其工作。dataIsAvailable()notify_one

场景 2:

在这里,生产者线程排在第一位,设置为 true 并调用 。然后,消费者线程来检查并发现它是真的 - 所以它不会调用 wait 并直接继续其工作。即使通知丢失,也不会在此构造中导致问题 - 消息已成功通过 dataIsAvailable 传递,并且避免了等待锁定。dataIsAvailable()notify_onedataIsAvailable()

在理想(非并发)世界中,这两种情况很可能足以描述可能的组合。但是在并发编程中,事情就不那么容易了。如图所示,有四个原子操作,每个线程两个。因此,当执行频率足够高时,所有可能的交错都会显现出来——我们必须找到那些仍然会导致问题的交错。

问题

下面是一个将导致程序锁定的组合:

使用者线程读取 ,在示例中为 false。然后,生产者设置为 true 并调用 notify。由于这种不吉利的操作交错,使用者线程调用等待,因为它被视为 false。这是可能的,因为使用者线程任务不是联合原子操作,而是可能被调度程序分隔并与一些其他任务交错 - 在本例中,由生产者线程执行的两个操作。这里的问题是,在调用 wait 之后,使用者线程将永远不会再次唤醒。此外,您可能已经注意到,共享变量 dataReady 不受互斥锁的保护 - 这使得出现问题的可能性更大。dataIsAvailable()dataIsAvailable()dataIsAvailable()

一个可能想到的解决方案的快速想法是执行两个操作 dataIsAvailable 并在锁定的互斥锁下等待。虽然这将有效地防止不同线程之间的任务交错,但它也会防止另一个线程再次修改 dataIsAvailable。

如此深入地讨论这些失败场景的一个原因是让你意识到并发行为的复杂性——即使是像我们现在正在讨论的简单协议。

因此,现在让我们看看上述问题的最终解决方案,从而了解我们通信协议的工作版本。

如上所示,我们正在缩小读取状态和进入等待之间的差距。我们正在读取锁下的状态(红色条),我们调用 wait still under the lock。然后,我们让 wait 释放锁,在一个原子步骤中进入等待状态。这之所以成为可能,是因为该方法能够将锁作为参数。然而,我们可以通过等待的锁并不是我们迄今为止经常使用的锁,而是必须在等待中暂时解锁的锁 - 适合此目的的锁将是我们在上一节中讨论的类型。

阅读排行