【问题标题】:Why is there no wait function for condition_variable which does not relock the mutex为什么不重新锁定互斥锁的条件变量没有等待功能
【发布时间】:2015-10-06 19:25:43
【问题描述】:

考虑以下示例。

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

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

【问题讨论】:

  • 为什么你认为这与 pthreads 有关系?
  • 您不能以这种方式可靠地使用condition_variable,因为可能会出现虚假唤醒。 condition_variable 通常保护某些状态,并用于等待该状态的某个谓词变为真。当它醒来时,你仔细检查谓词,如果它是假的,就回去睡觉。但当然,对共享状态的访问必须在互斥体下。
  • 像往常一样,您将条件变量与信号混淆。 POSIX 没有信号,虽然可以使用条件变量来模仿它们,但它们本身并不是信号。
  • @CaptainObvlious,事实上这个问题与 pthread 有关。 CPP 线程实现是与 Posix 的一对一映射,在我看来,这是一个不受欢迎的事件。

标签: c++ multithreading c++11 mutex condition-variable


【解决方案1】:

您误解了您的代码的作用。

您在// 1 上的代码完全不受阻塞。 condition_variables 可以(并且将会!)有虚假的唤醒——他们可以毫无理由地唤醒。

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

正确使用condition_variable 需要三件事:

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

互斥锁保护的数据被修改(在mutex下)。然后(mutex 可能未启用),condition_variable 会收到通知。

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

如果不是有效的唤醒,你就回去等待。

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

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

如果你想知道为什么条件变量是这样设计的,可能是因为这种设计比“可靠”的设计更有效(无论出于何种原因),而且 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() 立即返回{}

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

如何使用上述内容的一个示例是预览状态源(例如,UI 线程)和一个或多个预览渲染器(速度不够快,无法在 UI 线程中运行)。

预览状态将预览状态转储到data_passer&lt;preview_state&gt; willy-nilly。渲染器竞争,其中一个抓住了它。然后他们渲染它,并将它传回(通过任何机制)。

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


关于比赛条件的以下问题。

如果正在传送的数据是atomic,我们不能没有“发送”端的互斥锁吗?

所以是这样的:

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 返回。

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

监听线程然后完成 lambda,并返回“虚假唤醒”。然后它会阻塞条件变量,并且永远不会注意到数据已发送。

在等待条件变量时使用的std::mutex 必须保护对条件变量“传递”的数据的写入(无论您做什么测试来确定唤醒是否是虚假的)和读取(在 lambda 中) ,或存在“丢失信号”的可能性。 (至少在一个简单的实现中:更复杂的实现可以为“常见情况”创建无锁路径,并且只在仔细检查中使用mutex。这超出了这个问题的范围。)

使用atomic 变量并不能解决这个问题,因为“确定消息是否为虚假”和“在条件变量中重新等待”这两个操作对于消息的“虚假性”而言必须是原子的。

【讨论】:

  • 我认为您的论点是正确的,除非检测虚假唤醒的变量是原子的。如果在等待之后立即处理循环并且上限是原子变量,则可能是这种情况。在虚假唤醒的情况下,不会处理该循环并且一切正常。
  • @first 我不知道有一个:但是,C++11 线程支持并不适合“最终用户”使用,它们是对低级原语的薄抽象。它们让您编写易于使用的库;它们不容易使用。
  • 也不得不不同意脚注1中的陈述。条件变量不传递消息,建议“永远不会收到没有人等待时发送的消息”是荒谬的。条件变量仅提供一种机制,让线程阻塞等待谓词为真,并让其他线程通知被阻塞的线程他们需要重新评估谓词。依赖于唤醒数量等于通知数量的设计从根本上是有缺陷的。在锁内通知只会增加争用。
  • @Casey 我知道我的错误是什么。我正在解决“使用原子发送数据”的情况,我们在根本不使用互斥锁的情况下修改“发送”的数据,而不是“在我们发送之前释放互斥锁”的情况。在发送安全之前释放互斥锁是正确的。
  • 对于原子情况,数据的修改和通知都不需要互斥锁的保护。在接收端if (data != 42) { std::unique_lock&lt;std::mutex&gt; _(guard); signal.wait(guard, [this]{ return data == 42 || abort; }); } 与双重检查模式配对时,只需在修改和通知之间“戳”互斥体,即data = 42; std::lock_guard&lt;std::mutex&gt;(guard); signal.notify_one();。是的,这看起来很傻,但是可以通过边缘触发而不是电平触发通知来实现无锁快速路径。这个问题有点矫枉过正。
猜你喜欢
  • 2015-07-12
  • 2014-02-28
  • 1970-01-01
  • 2010-12-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-01-10
  • 2010-11-06
相关资源
最近更新 更多