为什么 condition_variable 没有不重新锁定互斥锁的等待函数

Why is there no wait function for condition_variable which does not relock the mutex

考虑以下示例。

std::mutex mtx;
std::condition_variable cv;

void f()
{
  {
    std::unique_lock<std::mutex>  lock( mtx );
    cv.wait( lock );  // 1
  }
  std::cout << "f()\n";
}

void g()
{
  std::this_thread::sleep_for( 1s );
  cv.notify_one();
}

int main()
{
  std::thread  t1{ f };
  std::thread  t2{ g };
  t2.join();
  t1.join();
}

g() "knows" f() 正在等待我想讨论的场景。 根据 cppreference.comg() 在调用 notify_one 之前不需要锁定互斥锁。现在,在标记为“1”的行中,cv 将释放互斥量并在发送通知后重新锁定它。 lock 的析构函数在那之后立即再次释放它。这似乎是多余的,尤其是因为锁定很昂贵。 (我知道在某些情况下需要锁定互斥量。但这里不是这种情况。)

为什么 condition_variable 没有函数“wait_nolock”一旦通知到达就不会重新锁定互斥体。如果答案是 pthreads 不提供这样的功能:为什么不能扩展 pthreads 来提供它?是否有实现所需行为的替代方案?

您误解了代码的作用。

您在行 // 1 上的代码完全没有阻塞。 condition_variables 可以(并且将会!)进行虚假唤醒——它们可以完全无缘无故地唤醒。

您有责任检查唤醒是否虚假。

正确使用 condition_variable 需要三件事:

  • 一个condition_variable
  • 一个mutex
  • mutex
  • 保护的一些数据

互斥锁保护的数据被修改(在mutex下)。然后(mutex 可能脱离),通知 condition_variable

在另一端,您锁定 mutex,然后等待条件变量。当你醒来时,你的 mutex 被重新锁定,你可以通过查看 mutex 保护的数据来测试唤醒是否虚假。如果是有效唤醒,则处理并继续。

如果不是有效的唤醒,您返回等待。

在你的情况下,你没有任何数据保护,你无法区分虚假唤醒和真实唤醒,你的设计不完整。

对于不完整的设计,您看不到重新锁定 mutex 的原因不足为奇:它已重新锁定,因此您可以安全地检查数据以查看唤醒是否虚假。

如果你想知道为什么这样设计条件变量,可能是因为这种设计比 "reliable" 更有效(无论出于何种原因),而不是公开更高级别的原语,C++ 公开了更低级别更高效的原语。

在此基础上构建更高级别的抽象并不难,但需要做出设计决策。这是建立在 std::experimental::optional 之上的一个:

template<class T>
struct data_passer {
  std::experimental::optional<T> data;
  bool abort_flag = false;
  std::mutex guard;
  std::condition_variable signal;

  void send( T t ) {
    {
      std::unique_lock<std::mutex> _(guard);
      data = std::move(t);
    }
    signal.notify_one();
  }
  void abort() {
    {
      std::unique_lock<std::mutex> _(guard);
      abort_flag = true;
    }
    signal.notify_all();
  }        
  std::experimental::optional<T> get() {
    std::unique_lock<std::mutex> _(guard);
    signal.wait( _, [this]()->bool{
      return data || abort_flag;
    });
    if (abort_flag) return {};
    T retval = std::move(*data);
    data = {};
    return retval;
  }
};

现在,每个 send 都可以使另一端的 get 成功。如果出现多个 send,则只有最新的一个被 get 消耗。如果设置 abort_flag,则 get() 立即 returns {};

以上支持多个消费者和生产者。

预览状态源(例如 UI 线程)和一个或多个预览渲染器(速度不够快 运行 在 UI 线程中)。

预览状态将预览状态转储到 data_passer<preview_state> 中。渲染器竞争,其中之一抓住了它。然后他们渲染它,并将它传回(通过任何机制)。

如果预览状态出现的速度快于渲染器消耗它们的速度,则只有最近的状态才有意义,所以较早的状态会被丢弃。但现有预览不会因为出现新状态而中止。


下面询问的有关竞争条件的问题。

如果正在通信的数据是atomic,我们不能没有"send"端的互斥量吗?

所以像这样:

template<class T>
struct data_passer {
  std::atomic<std::experimental::optional<T>> data;
  std::atomic<bool> abort_flag = false;
  std::mutex guard;
  std::condition_variable signal;

  void send( T t ) {
    data = std::move(t); // 1a
    signal.notify_one(); // 1b
  }
  void abort() {
    abort_flag = true;   // 1a
    signal.notify_all(); // 1b
  }        
  std::experimental::optional<T> get() {
    std::unique_lock<std::mutex> _(guard); // 2a
    signal.wait( _, [this]()->bool{ // 2b
      return data.load() || abort_flag.load(); // 2c
    });
    if (abort_flag.load()) return {};
    T retval = std::move(*data.load());
    // data = std::experimental::nullopt;  // doesn't make sense
    return retval;
  }
};

上述方法无效。

我们从监听线程开始。它执行步骤 2a,然后等待 (2b)。它在步骤 2c 评估条件,但还没有从 lambda return。

广播线程然后执行步骤 1a(设置数据),然后向条件变量发送信号。此时,没有人在等待条件变量(lambda 中的代码不算!)。

侦听线程然后完成 lambda,并且 returns "spurious wakeup"。然后它会阻塞条件变量,并且永远不会注意到数据已发送。

等待条件变量时使用的 std::mutex 必须保护条件变量对数据 "passed" 的写入(无论您做什么测试以确定唤醒是否虚假),以及读取(在 lambda 中),或者存在 "lost signals" 的可能性。 (至少在一个简单的实现中:更复杂的实现可以为 "common cases" 创建无锁路径,并且只在双重检查中使用 mutex 。这超出了这个问题的范围。)

使用atomic变量并不能解决这个问题,因为"determine if the message was spurious"和"rewait in the condition variable"这两个操作对于消息的"spuriousness"必须是原子的.