等待使用std :: atomic标志和std :: condition_variable的工作线程

时间:2019-12-03 21:33:50

标签: c++ multithreading c++17

这是一个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};

2 个答案:

答案 0 :(得分:0)

std:atomicstd:condition_variable的组合使用是非常规的,应避免使用, 但是,如果您在代码审查中遇到此问题并需要确定是否需要补丁,则分析该行为可能会很有趣。

我相信有两个问题:

  1. 由于ready_flag不受std:mutex的保护,因此不能依靠保证wait从{{1}唤醒后线程1会观察到更新后的值。 }。 如果平台延迟了线程2中对notify_one的存储,线程1可能会看到旧值(ready_flag)并再次输入false(可能导致死锁)。
    是否可以延迟存储取决于您的平台。在wait之类的有序平台上,您可能很安全,但同样,C ++标准也不保证。
    另外请注意,在此处使用更强的内存排序无济于事。

  2. 可以说,存储没有延迟,一旦X86醒来,wait就会加载ready_flag
    这次,基于您正在使用的内存顺序,线程2中true的存储与线程1中的负载同步,线程1现在可以安全地访问线程2写入的修改状态。

    但是,这只能运行一次。您无法重置ready_flag并再次写入共享状态。这将导致数据争用,因为两个线程现在可以不同步地访问共享状态

  

是否可以进一步放松这段代码

由于您正在修改锁外部的共享状态,因此必须进行ready_flag上的释放/获取顺序。

要使其成为可移植的解决方案,请在由互斥锁保护的情况下访问共享状态和ready_flagready_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}是必需的。