这是一个C ++ 17代码段,线程在该代码段上等待另一个代码到达特定阶段:
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();
据我了解,这是使用原子标记和条件变量来实现目标的有效方法。例如,这里不需要使用std::memory_order_seq_cst
。
是否可以进一步放松此代码?例如:
std::memory_order_relaxed
中使用ready_flag.load()
std::atomic_thread_fence()
而不是std::lock_guard{m};
答案 0 :(得分:0)
std:atomic
和std:condition_variable
的组合使用是非常规的,应避免使用,
但是,如果您在代码审查中遇到此问题并需要确定是否需要补丁,则分析该行为可能会很有趣。
我相信有两个问题:
由于ready_flag
不受std:mutex
的保护,因此不能依靠保证wait
从{{1}唤醒后线程1会观察到更新后的值。 }。
如果平台延迟了线程2中对notify_one
的存储,线程1可能会看到旧值(ready_flag
)并再次输入false
(可能导致死锁)。
是否可以延迟存储取决于您的平台。在wait
之类的有序平台上,您可能很安全,但同样,C ++标准也不保证。
另外请注意,在此处使用更强的内存排序无济于事。
可以说,存储没有延迟,一旦X86
醒来,wait
就会加载ready_flag
。
这次,基于您正在使用的内存顺序,线程2中true
的存储与线程1中的负载同步,线程1现在可以安全地访问线程2写入的修改状态。
但是,这只能运行一次。您无法重置ready_flag
并再次写入共享状态。这将导致数据争用,因为两个线程现在可以不同步地访问共享状态
是否可以进一步放松这段代码
由于您正在修改锁外部的共享状态,因此必须进行ready_flag
上的释放/获取顺序。
要使其成为可移植的解决方案,请在由互斥锁保护的情况下访问共享状态和ready_flag
(ready_flag
可以是普通的ready_flag
)。
这就是设计该机制的方式。
bool
在调用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();
之前解锁互斥锁是一种优化。有关更多详细信息,请参见this question。
答案 1 :(得分:0)
首先:此代码确实有效。调用lock_guard
之前的notify_one
确保等待线程在唤醒时看到ready_flag
的正确值,无论是由于虚假唤醒还是由于调用notify_one
。
第二:如果仅显示对ready_flag
的访问权限,则使用atomic
是多余的。在写入线程上ready_flag
范围内将写入移动到lock_guard
,并使用更简单,更常规的模式。
如果坚持使用这种模式,那么是否可以使用memory_order_relaxed
取决于所需的排序语义。
如果设置ready_flag
的线程也写入其他将由读取器线程读取的对象,则您需要语义,以确保数据正确可见:读取器线程可能锁定了互斥锁,并在写入器线程锁定了互斥锁之前查看了ready_flag
的新值,在这种情况下,互斥锁本身将不提供任何排序保证。
如果设置ready_flag
的线程没有接触其他数据,或者该数据受其他互斥锁或其他同步机制保护,那么您可以在任何地方使用memory_order_relaxed
,因为它仅仅是您关心的ready_flag
本身的值,而不是其他任何写入的顺序。
atomic_thread_fence
在任何情况下都不会对这段代码有所帮助。如果您使用条件变量,那么lock_guard{m}
是必需的。