一般来说,处理虚假唤醒的正确方法是什么?

What's the correct way to deal with spurious wakeups, in general?

在下面的选项中,有没有正确的方法来处理使用条件变量时的虚假唤醒?

1) 使用布尔值

wait(unique_lock_ul) 放入无限 while 循环
unique_lock<mutex> ul(m); 
while(!full)
  cv.wait(ul);

2) 同 if

unique_lock<mutex> ul(m); 
if(!full)
  cv.wait(ul);

3) 在 wait() 中放置一个条件,例如使用 lambda 函数

unique_lock<mutex> ul(m); 
cv.wait(ul, [&](){return !full;});

如果 none 是正确的,如何轻松处理虚假唤醒?

我对 C++ 中的条件变量相当陌生,我不确定我阅读的某些代码是否处理虚假唤醒的情况。

简短的回答是,您的代码可能正确也可能错误;您没有确切说明 full 是如何被操纵的。

C++ 代码的个别位永远不是线程安全的。线程安全是关系属性的代码;如果两位代码永远不会导致竞争条件,那么它们可以是线程安全的。

但是一点代码永远都不是线程安全的;说某物是线程安全的就像说某物是“相同高度”一样。


“monkey see monkey do”条件变量模式是这样的:

template<class T>
class cv_bundle {
  std::mutex m;
  T payload;
  std::condition_variable cv;
public:
  explicit cv_bundle( T in ):payload(std::move(in)) {}

  template<class Test, class Extract>
  auto wait( Test&& test, Extract&& extract ) {
    std::unique_lock<std::mutex> l(m);
    cv.wait( l, [&]{ return test(payload); } );
    return extract(payload);
  }
  template<class Setter>
  void load( Setter&& setter, bool only_one = true ) {
    std::unique_lock<std::mutex> l(m);
    bool is_set = setter( payload );

    if (!is_set) return; // nothing to notify
    if (only_one)
      cv.notify_one();
    else
      cv.notify_all();
  }
};

test 需要一个 T& payload 并且 return 如果有东西要消耗(即唤醒不是虚假的)则为真。

extract 接受 T& payload 和 return 任何你想从中得到的信息。它通常应该重置有效负载。

settertest 将 return true 的方式修改 T& payload。如果这样做,它 returns true。如果它选择不这样做,它 returns false.

所有 3 个都在互斥锁定访问 T payload 中调用。

现在,您可以生成变体,但很难做到正确。例如,不要假设原子负载意味着您​​不必锁定互斥量。

虽然我将这 3 个东西捆绑在一起,但您可以将单个互斥锁用于一堆条件变量,或者将互斥锁用于不仅仅是条件变量。有效载荷可以是一个布尔值、一个计数器、一个数据向量,或者更奇怪的东西;通常,它必须始终受到互斥锁的保护。如果它是原子的,在被修改的值和通知之间的开放时间间隔中的某个时间点,互斥体必须被锁定,否则你有丢失通知的风险。

手动循环控制而不是传入lambda是一个修改,但是描述什么样的手动循环是合法的,哪些是竞争条件是一个复杂的问题。

实际上,除非我有非常充分的理由,否则我避免离开这种猴子看猴子做的“货物崇拜”风格的条件变量使用。然后我被迫阅读 C++ 内存和线程模型,这让我很不高兴,这意味着我的代码很可能不正确。

请注意,如果传入的任何 lambda 表达式返回并回调 cv_bundle,我显示的代码将不再有效。

1 或 3 种方法都可以处理虚假唤醒(假设 full 修改受同一个互斥体保护)除了谓词条件错误,它应该是:

unique_lock<mutex> ul(m); 
cv.wait(ul, [&](){return full;});

使此代码等于变体 1。

变体 2 并不好,因为不会重新检查虚假唤醒等待条件,这与其他 2 种情况不同。