C++11:为什么std::condition_variable使用std::unique_lock?

84
我对在使用std::condition_variable时,std::unique_lock的作用有些困惑。根据我理解文档,std::unique_lock基本上是一个臃肿的锁定保护器,并且可以在两个锁之间交换状态。
到目前为止,我一直使用pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)实现这个目的(我猜这就是STL在posix上使用的方式)。它接受一个互斥量,而不是一个锁。
这里有什么区别吗?std::condition_variable使用std::unique_lock是否是一种优化?如果是,它具体是如何更快?

5
你是否对为什么需要在条件变量中使用锁/互斥锁感到困惑?或者对锁和互斥锁之间的区别感到困惑?或者对为什么条件变量使用独占锁而不是互斥锁感到困惑? - Brady
3
为什么条件变量使用唯一锁而不是互斥锁? - lucas clemente
3个回答

117

那么没有技术原因吗?

我赞同cmeerw的回答,因为我认为他给出了一个技术原因。让我们来看一下。假设委员会决定让condition_variable等待mutex。这里是使用该设计的代码:

void foo()
{
    mut.lock();
    // mut locked by this thread here
    while (not_ready)
        cv.wait(mut);
    // mut locked by this thread here
    mut.unlock();
}

这正是一个人不应该使用condition_variable的方式。在标有以下内容的区域:

// mut locked by this thread here

存在一种异常安全问题,而且这是一个严重的问题。如果在这些区域(或者通过cv.wait本身)抛出了异常,则互斥锁的锁定状态将泄漏,除非在某处也放置try / catch来捕获异常并解锁它。但那只是要求程序员编写更多代码。

假设程序员知道如何编写异常安全的代码,并且知道使用unique_lock来实现它。现在代码看起来像这样:

void foo()
{
    unique_lock<mutex> lk(mut);
    // mut locked by this thread here
    while (not_ready)
        cv.wait(*lk.mutex());
    // mut locked by this thread here
}

这样已经好很多了,但仍不是一个很好的情况。 condition_variable 接口让程序员费尽心思才能使其正常工作。如果 lk 不小心没有引用到互斥量,则可能会发生空指针解引用。而且,condition_variable::wait 没有办法检查此线程是否拥有 mut 上的锁。

哦,刚想起来,程序员还可能选择错误的 unique_lock 成员函数来公开互斥量。在这里使用 *lk.release() 将是灾难性的。

现在让我们看一下如何使用实际的 condition_variable API 来编写代码,该 API 接受一个 unique_lock<mutex>

void foo()
{
    unique_lock<mutex> lk(mut);
    // mut locked by this thread here
    while (not_ready)
        cv.wait(lk);
    // mut locked by this thread here
}
  1. 这段代码尽可能简单。
  2. 它是异常安全的。
  3. wait函数可以检查lk.owns_lock(),并在其为false时抛出异常。

这些是驱动condition_variable API设计的技术原因。

此外,condition_variable::wait没有使用lock_guard<mutex>,因为lock_guard<mutex>表示:我拥有此互斥锁的锁,直到lock_guard<mutex>解构。但是当您调用condition_variable::wait时,您会隐式释放互斥锁上的锁。因此,该操作与lock_guard使用案例/语句不一致。

我们需要unique_lock以便可以从函数返回锁,将其放入容器中,并以异常安全的方式非范围模式锁定/解锁互斥锁,因此unique_lockcondition_variable::wait的自然选择。

更新

在下面的评论中,bamboon建议我对比一下condition_variable_any,因此让我们来看看:

问题:为什么condition_variable::wait没有模板化,以便我可以将任何Lockable类型传递给它?

答案:

这是非常有用的功能。例如这篇文章演示了在条件变量上以共享模式等待shared_lock(rwlock)的代码(这在posix世界中是无法想象的,但非常有用)。但是,这个功能更为昂贵。

因此,委员会引入了一个具有此功能的新类型:

`condition_variable_any`

通过使用这个condition_variable适配器,可以等待任何可锁定类型。如果它有lock()unlock()成员函数,那么就可以使用。一个正确的condition_variable_any的实现需要有一个condition_variable数据成员和一个shared_ptr<mutex>数据成员。

因为这个新功能比基本的condition_variable::wait更昂贵,并且因为condition_variable是一个如此底层的工具,所以这个非常有用但更昂贵的功能被放入一个单独的类中,这样只有在使用时才需要付出代价。


你能澄清一下“它”指的是什么吗?你是在谈论lock_guard,还是condition_variable,或者可能是condition_variable::wait - Howard Hinnant
抱歉,不用了。我完全忘记了 condition_variable_any - Stephan Dollberg
啊,模板中的 condition_variable::wait。是的,condition_variable_any 是正确的选择。而且之所以没有将这个功能整合到 condition_variable 中,是因为它更加消耗资源。而 condition_variable 是一种非常底层的工具,需要尽可能高效。只有在使用 condition_variable_any 的时候才会支付额外的功能费用。 - Howard Hinnant
好的,谢谢您提供额外的信息。也许您可以将其添加到您的答案中,因为有些人可能会在使用普通互斥量时误用condition_variable_any - Stephan Dollberg
5
哇,你的回答让我感到非常惊艳。非常感谢你提供这么详细的内容! - lucas clemente
显示剩余2条评论

37

这基本上是一种API设计决策,通过默认设置API尽可能安全(附加开销被视为可以忽略)。通过要求传递unique_lock而不是原始的mutex,API的用户被指向编写正确的代码(在异常存在的情况下)。

近年来,C++语言的重点已经转向通过默认设置使其更安全(但仍允许用户尝试努力并自毁)。


那么没有技术上的原因吗? - lucas clemente

0
实现std::condition_variable使用了Linux中的条件变量pthread_condpthread_cond_wait_*函数需要一个mutex作为输入变量。只有unique_lock有成员函数可以获取mutex。其他的如lock_guardscoped_lock都不符合这个要求。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接