问题
Here is a C++17 snippet where on thread waits for another to reach certain stage:
std::condition_variable cv;
std::atomic<bool> ready_flag{false};
std::mutex m;
// thread 1
... // start a thread, then wait for it to reach certain stage
auto lock = std::unique_lock(m);
cv.wait(lock, [&]{ return ready_flag.load(std::memory_order_acquire); });
// thread 2
... // modify state, etc
ready_flag.store(true, std::memory_order_release);
std::lock_guard{m}; // NOTE: this is lock immediately followed by unlock
cv.notify_all();
As I understand this is a valid way to use atomic flag and condition variable to achieve the goal. For example there is no need to use std::memory_order_seq_cst here.
Is it possible to relax this code even further? For example:
- maybe using
std::memory_order_relaxedinready_flag.load() - maybe using
std::atomic_thread_fence()instead ofstd::lock_guard{m};
回答1:
The combined use of a std:atomic and std:condition_variable is unconventional and should be avoided,
but it can be interesting to analyse the behavior if you come across this in a code review and need to decide if a patch is required.
I believe there are 2 problems:
Since
ready_flagis not protected by thestd:mutex, you cannot rely on the guarantee that thread 1 will observe the updated value oncewaitwakes up fromnotify_one. If the store toready_flagin thread 2 is delayed by the platform, thread 1 may see the old value (false) and enterwaitagain (possibly causing a deadlock).
Whether a delayed store is possible depends on your platform. On a strongly ordered platform such asX86, you are probably safe, but again, no guarantees from the C++ standard.
Also note that using a stronger memory ordering does not help here.let's say, the store is not delayed and once
waitwakes up,ready_flagloadstrue.
This time, based on the memory ordering you are using, the store toready_flagin thread 2, synchronizes with the load in thread 1 which can now safely access the modified state written by thread 2.But, this only works one time. You cannot reset
ready_flagand write to the shared state again. That would introduce a data race since the shared state can now be accessed unsynchronized by both threads
Is it possible to relax this code even further
Because you are modifying the shared state outside the lock, release/acquire ordering on ready_flag is necessary for synchronization.
To make this a portable solution, access both the shared state and ready_flag while protected by the mutex (ready_flag can be a plain bool).
This is how the mechanism is designed to be used.
std::condition_variable cv;
bool ready_flag{false}; // not atomic
std::mutex m;
// thread 1
... // start a thread, then wait for it to reach certain stage
auto lock = std::unique_lock(m);
cv.wait(lock, [&] { return ready_flag; });
ready_flag = false;
// access shared state
// thread 2
auto lock = std::unique_lock(m);
... // modify state, etc
ready_flag = true;
lock.unlock(); // optimization
cv.notify_one();
Unlocking the mutex before the call to notify_one is an optimization. See this question for more details.
回答2:
Firstly: this code is indeed valid. The lock_guard prior to the notify_one call ensures that the waiting thread will see the correct value of ready_flag when it wakes, whether that is due to a spurious wake, or due to the call to notify_one.
Secondly: if the only accesses to the ready_flag are those shown here, then the use of atomic is overkill. Move the write to ready_flag inside the scope of the lock_guard on the writer thread and use a simpler, more conventional pattern.
If you stick with this pattern, then whether or not you can use memory_order_relaxed depends on the ordering semantics you require.
If the thread that sets the ready_flag also writes to other objects which will be read by the reader thread, then you need the acquire/release semantics in order to ensure that the data is correctly visible: the reader thread may lock the mutex and see the new value of ready_flag before the writer thread has locked the mutex, in which case the mutex itself would provide no ordering guarantees.
If there is no other data touched by the thread that sets the ready_flag, or that data is protected by another mutex or other synchronization mechanism, then you can use memory_order_relaxed everywhere, as it is only the value of ready_flag itself that you care about, and not the ordering of any other writes.
atomic_thread_fence doesn't help with this code under any circumstances. If you are using a condition variable, then the lock_guard{m} is required.
来源:https://stackoverflow.com/questions/59165685/waiting-on-worker-thread-using-stdatomic-flag-and-stdcondition-variable