我有以下代码,注释行上的死锁。基本上f1和f2作为程序中的单个线程运行。 f1期望i为1并递减它,通知cv。 f2期望i为0并递增它,通知cv。我假设如果f2将i增加到1,则调用死锁,调用cv.notify(),然后f1读取过时的i值(为0),因为互斥锁和i之间没有内存同步,然后等待并且永远不会被唤醒起来。然后f2也进入睡眠状态,现在两个线程都在等待一个永远不会被通知的cv。
如何编写此代码以便不会发生死锁?基本上我想要实现的是拥有一些由两个线程更新的原子状态。如果其中一个线程的状态不正确,我不想旋转;相反,我想使用cv功能(或类似的东西)在值正确时唤醒线程。
我使用g ++ - 7用O3编译代码(尽管O0和O3都发生了死锁)。
#include <atomic>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
std::atomic_size_t i{0};
std::mutex mut;
std::condition_variable cv;
void f1() {
while (1) {
{
std::unique_lock<std::mutex> lk(mut);
cv.wait(lk, []() { return i.load() > 0; }); // deadlocks
}
--i;
cv.notify_one();
std::cout << "i = " << i << std::endl; // Only to avoid optimization
}
}
void f2() {
while (1) {
{
std::unique_lock<std::mutex> lk(mut);
cv.wait(lk, []() { return i.load() < 1; }); // deadlocks
}
++i;
cv.notify_one();
std::cout << "i = " << i << std::endl; // Only to avoid optimization
}
}
int main() {
std::thread t1(f1);
std::thread t2(f2);
t1.join();
t2.join();
return 0;
}
编辑:cout只是为了避免编译器优化。
答案 0 :(得分:8)
我认为问题是i
的值可能被改变,并且在另一个线程评估notify_one
之后但在lambda调用返回并且cv恢复等待之前的时间间隔内可以调用return i.load() > 0;
。这样,原子变量的变化不会被另一个线程观察到,并且没有人将其唤醒以再次检查。这可以通过在更改变量时锁定互斥量来解决,但这样做会破坏原子的目的。
答案 1 :(得分:4)
我认为VTT的答案是正确的,只是想表明会发生什么。首先,代码可以重写为以下形式:
void f1() {
while (1) {
{
std::unique_lock<std::mutex> lk(mut);
while (i == 0) cv.wait(lk);
}
--i;
cv.notify_one();
}
}
void f2() {
while (1) {
{
std::unique_lock<std::mutex> lk(mut);
while (i >= 1) cv.wait(lk);
}
++i;
cv.notify_one();
}
}
现在,请考虑以下时间线,i
最初为0
:
time step f1: f2:
========= ================= ================
1 locks mut
2 while (i >= 1) F
3 unlocks mut
4 locks mut
5 while (i == 0) T
6 ++i;
7 cv.notify_one();
8 cv.wait(lk);
9 unlocks mut(lk)
10 locks mut
11 while (i >= 1) T
12 cv.wait(lk);
有效地,f1
在i
为1
时等待。两个线程现在都在等待阻塞状态。
解决方案是将i
的修改放入锁定的部分。然后,i
甚至不需要是原子变量。
答案 2 :(得分:3)
当线程不拥有互斥锁时,您调用cv.notify_one();
。它可能导致通知被发送到空。想象f2
在f1
之前开始。 f2
来电cv.notify_one();
,但f1
尚未cv.wait
。
获取的互斥锁保证f2
位于std::unique_lock<std::mutex> lk(mut)
或等待通知。
#include <atomic>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>
std::atomic_size_t i{0};
std::mutex mut;
std::condition_variable cv;
void f1() {
while (1) {
std::size_t ii;
{
std::unique_lock<std::mutex> lk(mut);
cv.wait(lk, []() { return i.load() > 0; });
ii = --i;
cv.notify_one();
}
std::cout << "i = " << ii << std::endl;
}
}
void f2() {
while (1) {
std::size_t ii;
{
std::unique_lock<std::mutex> lk(mut);
cv.wait(lk, []() { return i.load() < 1; });
ii = ++i;
cv.notify_one();
}
std::cout << "i = " << ii << std::endl;
}
}
int main() {
std::thread t1(f1);
std::thread t2(f2);
t1.join();
t2.join();
return 0;
}
BTW std::atomic_size_t i
可能是std::size_t i
。
答案 3 :(得分:-3)
由于 i 是原子的,因此无需使用互斥锁来保护其修改。
等待 f1 和 f2 中的条件变量等待,除非发生虚假唤醒,因为条件变量从未通知过。由于无法保证虚假唤醒,我建议在等待条件变量之前检查条件,并最终通知条件变量为另一个线程。
您的代码还有另一个问题。功能 f1 和 f2 都不会结束。因此,您的主函数将等待加入其线程。