3

在以下选项中,在使用条件变量时是否有任何正确的方法来处理虚假唤醒?

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;});

如果这些都不正确,如何轻松处理虚假唤醒?

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

4

2 回答 2

4

简短的回答是,您的代码可能正确或错误;你没有确切地展示如何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();
  }
};

testT& payload如果有东西要消耗(即唤醒不是虚假的),则接受 a并返回 true。

extract接受 aT& payload并返回你想要的任何信息。它通常应该重置有效载荷。

setter以将返回T& payload的方式修改. 如果这样做,它会返回. 如果它选择不这样做,它会返回。testtruetruefalse

所有 3 个都在对T payload.

现在,您可以对此产生变化,但这样做很难做到正确。例如,不要假设原子有效负载意味着您​​不必锁定互斥锁。

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

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

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

请注意,如果传入的任何 lambda 并回调到cv_bundle我显示的代码中不再有效。

于 2018-09-05T19:48:36.010 回答
3

1 或 3 方式都可以处理虚假唤醒(假设full修改受相同的互斥锁保护),除非您的谓词条件错误,它应该是:

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

使此代码等于变体 1。

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

于 2018-09-05T19:53:16.590 回答