问题
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