C ++ 17 atomics和condition_variable deadlock

时间:2018-04-03 05:17:47

标签: c++ multithreading atomic c++17 condition-variable

我有以下代码,注释行上的死锁。基本上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只是为了避免编译器优化。

4 个答案:

答案 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);

有效地,f1i1时等待。两个线程现在都在等待阻塞状态。

解决方案是将i的修改放入锁定的部分。然后,i甚至不需要是原子变量。

答案 2 :(得分:3)

当线程不拥有互斥锁时,您调用cv.notify_one();。它可能导致通知被发送到空。想象f2f1之前开始。 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 都不会结束。因此,您的函数将等待加入其线程。