简体   繁体   English

C ++ 11 std :: condition_variable:我们可以直接将锁传递给通知的线程吗?

[英]C++11 std::condition_variable: can we pass our lock directly to the notified thread?

I'm learning about C++11 concurrency, where my only prior experience with concurrency primitives was in Operating Systems class six years ago, so be gentle, if you can. 我正在学习C ++ 11并发性,其中我以前唯一的并发原语经验是六年前的操作系统类,所以如果可以,请保持温和。

In C++11, we can write 在C ++ 11中,我们可以编写

std::mutex m;
std::condition_variable cv;
std::queue<int> q;

void producer_thread() {
    std::unique_lock<std::mutex> lock(m);
    q.push(42);
    cv.notify_one();
}

void consumer_thread() {
    std::unique_lock<std::mutex> lock(m);
    while (q.empty()) {
        cv.wait(lock);
    }
    q.pop();
}

This works fine, but I'm offended by the need to wrap cv.wait in a loop. 这样可以正常工作,但我觉得需要在循环中包装cv.wait The reason we need the loop is clear to me: 我们需要循环的原因对我来说很清楚:

Consumer (inside wait())       Producer            Vulture

release the lock
sleep until notified
                               acquire the lock
                               I MADE YOU A COOKIE
                               notify Consumer
                               release the lock
                                                   acquire the lock
                                                   NOM NOM NOM
                                                   release the lock
acquire the lock
return from wait()
HEY WHERE'S MY COOKIE                              I EATED IT

Now, I believe one of the cool things about unique_lock is that we can pass it around, right? 现在,我相信unique_lock一个很酷的事情是我们可以传递它,对吧? So it would be really elegant if we could do this instead: 如果我们能做到这一点,那将是非常优雅的:

Consumer (inside wait())       Producer

release the lock
sleep until notified
                               acquire the lock
                               I MADE YOU A COOKIE
                               notify and yield(passing the lock)
wake(receiving the lock)
return from wait()
YUM
release the lock

Now there's no way for the Vulture thread to swoop in, because the mutex remains locked all the way from I MADE YOU A COOKIE to YUM . 现在没有办法让Vulture线程突然进入,因为互斥锁一直被锁定,从I MADE YOU A COOKIEYUM Plus, if notify() requires that you pass a lock, that's a good way to ensure that people actually lock the mutex before calling notify() (see Signalling a condition variable (pthreads) ). 另外,如果notify()要求您传递锁,那么这是确保人们在调用notify()之前实际锁定互斥锁的好方法(请参阅信号条件变量(pthreads) )。

I'm pretty sure that C++11 doesn't have any standard implementation of this idiom. 我很确定C ++ 11没有这个习惯用法的任何标准实现。 What's the historical reason for that (is it just that pthreads didn't do it? and then why is that )? 这是什么历史原因(只是pthreads没有这样做?然后为什么会这样 )? Is there a technical reason that an adventurous C++ coder couldn't implement this idiom in standard C++11, calling it perhaps my_better_condition_variable ? 有一个技术上的原因,一个冒险的C ++编码器无法在标准C ++ 11中实现这个习惯用法,称它可能是my_better_condition_variable

I also have a vague feeling that maybe I'm reinventing semaphores, but I don't remember enough from school to know if that's accurate or not. 我也有一种模糊的感觉,也许我正在重新发明信号量,但我不记得从学校知道这是否准确。

The ultimate answer is because pthreads didn't do it. 最终的答案是因为pthreads没有这样做。 C++ is a language that encapsulates operating system functionality. C ++是一种封装操作系统功能的语言。 C++ is not an operating system or platform. C ++不是操作系统或平台。 And so it encapsulates the existing functionality of operating systems such as linux, unix and windows. 因此它封装了Linux,unix和windows等操作系统的现有功能。

However pthreads also has a good rationale for this behavior as well. 然而,pthreads也有很好的理由来解释这种行为。 From the Open Group Base Specifications: 来自Open Group Base规格:

The effect is that more than one thread can return from its call to pthread_cond_wait() or pthread_cond_timedwait() as a result of one call to pthread_cond_signal(). 结果是,由于对pthread_cond_signal()的一次调用,多个线程可以从其对pthread_cond_wait()或pthread_cond_timedwait()的调用返回。 This effect is called "spurious wakeup". 这种效应称为“虚假唤醒”。 Note that the situation is self-correcting in that the number of threads that are so awakened is finite; 注意,情况是自我纠正的,因为被唤醒的线程数是有限的; for example, the next thread to call pthread_cond_wait() after the sequence of events above blocks. 例如,在块上方的事件序列之后调用pthread_cond_wait()的下一个线程。

While this problem could be resolved, the loss of efficiency for a fringe condition that occurs only rarely is unacceptable, especially given that one has to check the predicate associated with a condition variable anyway. 虽然可以解决这个问题,但是很少发生的边缘条件的效率损失是不可接受的,特别是考虑到必须检查与条件变量相关联的谓词。 Correcting this problem would unnecessarily reduce the degree of concurrency in this basic building block for all higher-level synchronization operations. 纠正此问题会不必要地降低此基本构建块中所有更高级别同步操作的并发度。

An added benefit of allowing spurious wakeups is that applications are forced to code a predicate-testing-loop around the condition wait. 允许虚假唤醒的另一个好处是应用程序被迫在条件等待周围编写谓词测试循环。 This also makes the application tolerate superfluous condition broadcasts or signals on the same condition variable that may be coded in some other part of the application. 这也使得应用程序可以容忍在应用程序的某些其他部分中编码的相同条件变量上的多余条件广播或信号。 The resulting applications are thus more robust. 由此产生的应用程序更加健壮。 Therefore, IEEE Std 1003.1-2001 explicitly documents that spurious wakeups may occur. 因此,IEEE Std 1003.1-2001明确记录了可能发生虚假唤醒的情况。

So basically the claim is that you can build my_better_condition_variable on top of a pthreads condition variable (or std::condition_variable ) fairly easily and without performance penalty. 所以基本上声称你可以相当容易地在pthreads条件变量(或std::condition_variable )之上构建my_better_condition_variable而不会降低性能。 However if we put my_better_condition_variable at the base level, then those clients who did not need the functionality of my_better_condition_variable would have to pay for it anyway. 但是,如果我们将my_better_condition_variable放在基本级别,那么那些不需要my_better_condition_variable功能的客户端无论如何都必须付费。

This philosophy of putting the fastest, most primitive design at the bottom of the stack, with the intent that better/slower things can be built on top of them, runs throughout the C++ lib. 这种将最快,最原始的设计放在堆栈底部的理念,其目的是在它们之上构建更好/更慢的东西,在整个C ++库中运行。 And where the C++ lib fails to follow this philosophy, clients are often (and rightly) irritated. 在C ++ lib无法遵循这一理念的地方,客户经常(并且正确地)烦恼。

If you don't want to write the loop you can use the overload that takes the predicate instead: 如果您不想编写循环,则可以使用带谓词的重载

cv.wait(lock, [&q]{ return !q.is_empty(); });

It is defined to be equivalent to the loop, so it works just as the original code. 它定义为等效于循环,因此它与原始代码一样。

Even if you could do this, the C++11 spec allows cv.wait() to unblock spuriously (to account for platforms that have that behavior). 即使您可以这样做,C ++ 11规范也允许cv.wait()虚假地解除阻塞(以考虑具有该行为的平台)。 So even if there are no vulture threads (setting aside the argument about whether or not they should exist), the consumer thread can't expect there to be a cookie waiting, and still has to check. 因此,即使没有vulture线程(抛开关于它们是否应该存在的争论),消费者线程也不能指望等待cookie,并且仍然需要检查。

I think this is not safe: 我认为这不安全:

void producer_thread() {
    std::unique_lock<std::mutex> lock(m);
    q.push(42);
    cv.notify_one();
}

You are still holding the lock when you notify another thread waiting for the lock. 当您通知另一个等待锁定的线程时,您仍然保持锁定。 So it could be that the other thread immediately awakes and tries to get the lock before the destructor called after cv.notify_one() releases the lock. 因此,可能是另一个线程立即唤醒并尝试 cv.notify_one()释放锁之后 cv.notify_one()析构函数之前获取锁。 That means the other thread goes back to wait eventually for ever. 这意味着另一个线程最终会回来等待。

So I think this should coded as: 所以我认为这应编码为:

void producer_thread() {
    std::unique_lock<std::mutex> lock(m);
    q.push(42);
    lock.unlock();
    cv.notify_one();
}

or if you don't like to unlock manually as 或者如果你不喜欢手动解锁

void producer_thread() {
    {
        std::unique_lock<std::mutex> lock(m);
        q.push(42);
    }
    cv.notify_one();
} 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM