C++17 atomics and condition_variable deadlock

浪子不回头ぞ 提交于 2020-05-26 12:24:22

问题


I have the following code, which deadlocks on the commented lines. Basically f1 and f2 run as individual threads in the program. f1 expects i to be 1 and decrements it, notifying the cv. f2 expects i to be 0 and increments it, notifying the cv. I assume the deadlock occurs if f2 increments i to 1, calls cv.notify(), then f1 reads a stale value of i (which is 0) because there is no memory synchronization between the mutex and i and then waits and never gets woken up. Then f2 also enters a sleep state and now both threads are waiting on a cv that will never be notified.

How can I write this code so that the deadlock does not occur? Basically what I want to be able to achieve is having some atomic state that gets updated by two threads. If the state is not correct in one of the threads, I do not want to spin; rather I want to use the cv functionality (or something similar) to wake the thread up when the value is correct.

I am using g++-7 to compile the code with O3 (although the deadlock occurs in both O0 and 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;
}

EDIT: cout is only to avoid compiler optimization.


回答1:


I think the problem is that the value of i could be altered and notify_one could be called in the interval after another thread evaluated return i.load() > 0; but before lambda call returns and cv resumes wait. This way the change of atomic variable won't be observed by another thread and there is no one to wake it up to check again. This could be solved by locking mutex when changing variable though doing so would defeat the purpose of atomic.




回答2:


I think VTT's answer is correct, just want to show what can happen. First, the code can be rewritten into the following form:

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();
   }
}

Now, consider the following time line, i being 0 initially:

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

Effectively, f1 waits at the moment when i is 1. Both threads are now waiting in a blocking state at once.


The solution would be to put modifications of i into locked sections. Then, i even does not need to be an atomic variable.




回答3:


You call cv.notify_one(); when a thread does not own the mutex. It may cause notification is sent to empty. Imagine f2 starts before f1. f2 calls cv.notify_one(); but f1 is not in cv.wait yet.

Acquired mutex guarantees that f2 is either in std::unique_lock<std::mutex> lk(mut) or waits for notification.

#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 could be std::size_t i.




回答4:


Since i is atomic, there is no need to guard its modification with the mutex.

The waits on the condition variable in f1 and f2 wait for ever unless a spurious wake-up occurs, because the condition variable was never notified. Since spurious wake-ups are not guaranteed, I suggest to check for the condition before waiting on the condition variable, and eventually notify the condition variable for the other thread.

There is another problem with your code. Both functions f1 and f2 will never end. So your main function will wait for ever joining its threads.



来源:https://stackoverflow.com/questions/49622713/c17-atomics-and-condition-variable-deadlock

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!