且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

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

更新时间:2022-10-15 18:13:01


因此没有技术原因?


我upvote cmeerw的答案,因为我相信他提出了技术原因。让我们通过它。让我们假设委员会决定让 condition_variable 等待一个 mutex 。下面是使用该设计的代码:

  void foo()
{
mut.lock
//这个线程锁定的mut
while(not_ready)
cv.wait(mut);
//由此线程锁定的mut
mut.unlock();
}

这是一个 a condition_variable 。在标有的区域中:

  //此线程锁定的mut 

有一个异常安全问题,这是一个严重的问题。如果在这些区域抛出异常(或者通过 cv.wait 本身),互斥体的锁定状态将被泄露,除非try / catch也被放在某处异常并解锁它。但是这只是你要求程序员写的更多代码。



让我们说程序员知道如何编写异常安全代码,并且知道使用 unique_lock 来实现它。现在代码如下:

  void foo()
{
unique_lock< mutex> lk(mut);
//这个线程锁定的变量
while(not_ready)
cv.wait(* lk.mutex());
//此线程在此锁定的mut
}

,但它仍然不是一个伟大的情况。 condition_variable 接口正在使程序员走出他的方式来让东西工作。如果 lk 意外地不引用互斥,则可能存在空指针解引用。并且 condition_variable :: wait 没有办法检查这个线程是否拥有 mut 上的锁。 p>

哦,只是记住,还有一个危险,程序员可以选择错误的 unique_lock 成员函数暴露互斥。 * lk.release()在这里会是灾难性的。



现在让我们看看代码是如何用实际 condition_variable 需要 unique_lock< mutex> 的API:

  void foo()
{
unique_lock< mutex> lk(mut);
//这个线程锁定的变量
while(not_ready)
cv.wait(lk);
//由此线程锁定的mut
}




  1. 此代码很简单,它可以得到。

  2. 这是异常安全的。

  3. wait 函数可以检查 lk.owns_lock(),如果 false ,则抛出异常。

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



condition_variable :: wait 不需要 lock_guard ,因为 lock_guard< mutex> 是怎么说的:我拥有这个互斥锁的锁,直到 lock_guard< mutex> destructs。但是当你调用 condition_variable :: wait 时,你隐式释放互斥锁。因此,此操作与 lock_guard 用例/语句不一致。



我们需要 unique_lock 无论如何,以便可以从函数返回锁,将它们放入容器,并以非作用域模式以异常安全方式锁定/解锁互斥锁,因此 unique_lock condition_variable :: wait 的自然选择。



更新 / p>

bamboon建议在下面的注释中对比 condition_variable_any ,所以这里:



问题:为什么不 condition_variable :: wait 模板化,以便我可以传递任何可锁定键入?



回答



这是真的很酷的功能。例如本白皮书演示了等待 shared_lock (rwlock)在共享模式下对条件变量(在posix世界中前所未闻,但非常有用)。



因此,委员会推出了一种具有此功能的新类型:

 `condition_variable_any` 

使用此 condition_variable 适配器 可以等待任何可锁定类型。如果它有成员 lock() unlock(),你很好去。正确实现 condition_variable_any 需要 condition_variable 数据成员和 shared_ptr< mutex> code>数据成员



因为这个新功能比您的基本 condition_variable :: wait ,并且因为 condition_variable 是这样一个低级工具,这个非常有用但更昂贵的功能被放入一个单独的类,所以你只需要支付它,如果你使用它。 / p>

I am a bit confused about the role of std::unique_lock when working with std::condition_variable. As far as I understood the documentation, std::unique_lock is basically a bloated lock guard, with the possibility to swap the state between two locks.

I've so far used pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) for this purpose (I guess that's what the STL uses on posix). It takes a mutex, not a lock.

What's the difference here? Is the fact that std::condition_variable deals with std::unique_lock an optimization? If so, how exactly is it faster?

so there is no technical reason?

I upvoted cmeerw's answer because I believe he gave a technical reason. Let's walk through it. Let's pretend that the committee had decided to have condition_variable wait on a mutex. Here is code using that design:

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

This is exactly how one shouldn't use a condition_variable. In the regions marked with:

// mut locked by this thread here

there is an exception safety problem, and it is a serious one. If an exception is thrown in these areas (or by cv.wait itself), the locked state of the mutex is leaked unless a try/catch is also put in somewhere to catch the exception and unlock it. But that's just more code you're asking the programmer to write.

Let's say that the programmer knows how to write exception safe code, and knows to use unique_lock to achieve it. Now the code looks like this:

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
}

This is much better, but it is still not a great situation. The condition_variable interface is making the programmer go out of his way to get things to work. There is a possible null pointer dereference if lk accidentally does not reference a mutex. And there is no way for condition_variable::wait to check that this thread does own the lock on mut.

Oh, just remembered, there is also the danger that the programmer may choose the wrong unique_lock member function to expose the mutex. *lk.release() would be disastrous here.

Now let's look at how the code is written with the actual condition_variable API that takes a 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. This code is as simple as it can get.
  2. It is exception safe.
  3. The wait function can check lk.owns_lock() and throw an exception if it is false.

These are technical reasons that drove the API design of condition_variable.

Additionally, condition_variable::wait doesn't take a lock_guard<mutex> because lock_guard<mutex> is how you say: I own the lock on this mutex until lock_guard<mutex> destructs. But when you call condition_variable::wait, you implicitly release the lock on the mutex. So that action is inconsistent with the lock_guard use case / statement.

We needed unique_lock anyway so that one could return locks from functions, put them into containers, and lock/unlock mutexes in non-scoped patterns in an exception safe way, so unique_lock was the natural choice for condition_variable::wait.

Update

bamboon suggested in the comments below that I contrast condition_variable_any, so here goes:

Question: Why isn't condition_variable::wait templated so that I can pass any Lockable type to it?

Answer:

That is really cool functionality to have. For example this paper demonstrates code that waits on a shared_lock (rwlock) in shared mode on a condition variable (something unheard of in the posix world, but very useful nonetheless). However the functionality is more expensive.

So the committee introduced a new type with this functionality:

`condition_variable_any`

With this condition_variable adaptor one can wait on any lockable type. If it has members lock() and unlock(), you are good to go. A proper implementation of condition_variable_any requires a condition_variable data member and a shared_ptr<mutex> data member.

Because this new functionality is more expensive than your basic condition_variable::wait, and because condition_variable is such a low level tool, this very useful but more expensive functionality was put into a separate class so that you only pay for it if you use it.