用C++中的std::condition_variable将线程置于死锁中会有风险吗

Do I have a risk to put a thread in deadlock with std::condition_variable in C++?

本文关键字:死锁 condition std 中的 C++ variable 线程 于死锁      更新时间:2023-10-16

这是一个更具理论性的问题。我已经看到了几个std::condition_variable的例子,它似乎让线程休眠,直到条件得到满足。这是某种意义上的种族旗帜。

但似乎如果一个变量总是被更改,那么线程可能会被唤醒以检查条件并再次入睡,因为它无法跟上谓词为真的情况,所以我可能会将线程放入";昏迷";。

那么,在这种情况下,当我总是更换std::condition_variable时,我使用它是否不安全?

这就是为什么无论何时使用条件变量都必须使用互斥锁的原因。我认为您忽略了线程开始等待并同时锁定互斥的事实。更改条件的线程在更改条件时也必须锁定互斥对象。

线程1:

Lock mutex
Check condition (it's false)
Unlock mutex and start waiting (these happen at the exact same time)
Finish waiting and lock mutex
Check condition (it's true)
Unlock mutex

线程2:

Lock mutex
Change condition
Unlock mutex
Notify condition variable

当条件已经成立时,线程1不可能等待。因为当互斥锁被锁定时,条件不会改变,并且直到线程1已经在等待时,互斥锁才会被解锁。

在线程2释放互斥体之后,但在线程2通知它之前,线程1可能会错误地醒来,看到条件为真,然后做一些其他事情,然后再次开始等待。然后,线程2将通知条件变量,而线程1将其视为虚假唤醒。

您似乎在谈论某种"活锁"。死锁是指一个或多个线程在等待锁释放时没有任何进展的情况。Livelock是类似的,但过程的状态不断变化,但仍然没有进展。我们也可以讨论"实际活锁",即线程没有足够的机会进行足够的进展。

对于观察者来说,活络和实际的活络往往看起来很像真正的僵局。

您应该设计程序逻辑以避免活锁。它可以是非琐碎的。例如,许多形式的锁都是不公平的。也就是说,当许多线程正在等待一个释放的锁时,第一个请求它的锁不能保证下一个会收到它。

事实上,许多操作系统锁本质上是不公平的,例如,给更容易唤醒的线程提供锁(例如,加载到核心并挂起),而不是更难唤醒的线程(从核心卸载并需要重新加载才能恢复执行)。

这个问题没有给出太多细节,所以很难诊断出情况。如果某个特定的线程(或线程类)需要优先级,则可以引入一个标志,在优先级线程正在等待(并且可以运行)的情况下告诉低优先级线程不要获取锁,例如(c && !(p && priority_waiting)),其中c是低优先级线程的逻辑条件,p是优先级线程的物理条件。

您当然应该避免线程在潜在的瞬态条件下等待的逻辑。假设您有一个监控器线程,它每1000个周期产生一次输出。像(cycles%1000 == 0)这样的等待条件很容易错过计数器的点击。它们应该更像(cycles-lcycles >=0),其中lcycles是监视器上次恢复处理时的循环计数。这确保了监视器通常会被赋予一个锁,而这个锁(出于实际目的)几乎永远不会被捕获。

在该示例中,线程正在等待(a)获取锁的机会和(b)某个瞬态条件。存在这样一种风险,即两者同时发生的情况很少见,线程可能被活锁或实际上被活锁,并且进展不足,

简而言之,确保线程在条件通过时恢复,而不是在条件完全通过时恢复

您可以引入一个严格的队列来给线程提供轮次。只是不要认为这就是你所拥有的,除非文件明确承诺公平。

这就是为什么condition_variable总是与锁(mutex)结合使用。

生产者和消费者逻辑都是在持有相同的锁的同时执行的,因此任何共享数据(例如队列、某些条件标志等)一次只能由一个线程访问。

在释放锁时通知condition_variable,等待线程在唤醒时获取锁。因此,实际上,锁被从生产者转移到消费者线程,并且不可能在消费者有机会看到它之前设置条件并取消设置,iff至少有一个其他线程已经在等待它。

换句话说,condition_variable::notify_one保证唤醒至少一个等待线程(condition_variable::wait)(如果有的话)。当然,生产者应该在调用notify_one之前或之后释放锁,以便让等待的线程继续进行。