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

Cla*_*tus 6 c++ multithreading mutex condition-variable c++11

请考虑以下示例.

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();
}
Run Code Online (Sandbox Code Playgroud)

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

为什么condition_variable没有函数" wait_nolock",一旦通知到达,它就不会重新锁定互斥锁.如果答案是pthreads没有提供这样的功能:为什么不能延长提供它的pthreads?是否有实现理想行为的替代方案?

Yak*_*ont 13

你误解了你的代码所做的事情.

您的在线代码// 1可以免费阻止. condition_variables可以(而且会!)有虚假的唤醒 - 他们可以毫无理由地醒来.

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

condition_variable正确使用需要3件事:

  • 一个 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;
  }
};
Run Code Online (Sandbox Code Playgroud)

现在,每一个都send可以使a get在另一端取得成功.如果出现多个send,则a只消耗最新的一个get.如果abort_flag设置了if和when ,则get()立即返回{};

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

可以使用上述内容的示例是预览状态源(例如,UI线程),以及一个或多个预览渲染器(其速度不足以在UI线程中运行).

预览状态将预览状态转储到data_passer<preview_state>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;
  }
};
Run Code Online (Sandbox Code Playgroud)

以上都无法奏效.

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

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

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

std::mutex使用,而等待条件变量必须警惕在写数据"通过"的条件变量(你做任何测试,以确定是否唤醒是虚假的),读(在lambda),或可能性"丢失的信号"存在.(至少在一个简单的实现中:更复杂的实现可以为"常见情况"创建无锁路径,并且仅mutex在双重检查中使用.这超出了这个问题的范围.)

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

  • 还必须不同意脚注1中的陈述.条件变量不传递消息,建议"当没有人等待时发送的消息永远不会被接收"是没有意义的.条件变量仅提供线程阻止等待谓词为真的机制,以及其他线程通知被阻塞的线程需要重新评估谓词的机制.依赖于唤醒次数等于通知次数的设计从根本上是有缺陷的.在锁内通知只会增加争用. (2认同)