15

我正在学习 C++11 并发,我之前唯一的并发原语经验是在六年前的操作系统课上,所以如果可以的话,请保持温和。

在 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();
}

这很好用,但是我对需要cv.wait循环包装感到生气。我们需要循环的原因对我来说很清楚:

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

现在,我相信其中一件很酷的事情unique_lock是我们可以传递它,对吧?因此,如果我们可以这样做,那将是非常优雅的:

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

现在 Vulture 线程无法突袭,因为互斥锁从I MADE YOU A COOKIE到一直保持锁定状态YUM。另外,如果notify()需要您传递锁,这是确保人们在调用之前实际锁定互斥锁的好方法notify()(请参阅Signaling a condition variable (pthreads))。

我很确定 C++11 没有这个习语的任何标准实现。其历史原因是什么(仅仅是 pthreads 没有这样做吗?那么为什么会这样)?是否有技术原因导致冒险的 C++ 编码人员无法在标准 C++11 中实现这个习语,也许称之为my_better_condition_variable

我也有一种模糊的感觉,也许我正在重新发明信号量,但我对学校的记忆不够,不知道这是否准确。

4

4 回答 4

10

最终的答案是因为 pthreads 没有这样做。C++ 是一种封装操作系统功能的语言。C++ 不是操作系统或平台。因此它封装了linux、unix和windows等操作系统的现有功能。

然而 pthreads 也有很好的理由来解释这种行为。来自开放组基本规范:

结果是,不止一个线程可以通过调用 pthread_cond_wait() 或 pthread_cond_timedwait() 作为一次调用 pthread_cond_signal() 的结果返回。这种效应称为“虚假唤醒”。请注意,这种情况是自我纠正的,因为如此唤醒的线程数是有限的;例如,在上述块的事件序列之后调用 pthread_cond_wait() 的下一个线程。

虽然这个问题可以解决,但对于很少发生的边缘条件的效率损失是不可接受的,特别是考虑到无论如何都必须检查与条件变量相关联的谓词。纠正这个问题会不必要地降低这个基本构建块中所有更高级别同步操作的并发程度。

允许虚假唤醒的另一个好处是应用程序被迫围绕条件等待编写谓词测试循环。这也使得应用程序可以容忍多余的条件广播或相同条件变量上的信号,这些条件变量可能被编码在应用程序的其他部分中。由此产生的应用程序更加健壮。因此,IEEE Std 1003.1-2001 明确记录了可能发生的虚假唤醒。

所以基本上声称你可以相当容易地my_better_condition_variable在 pthreads 条件变量(或 )之上构建,std::condition_variable而不会降低性能。但是,如果我们放在my_better_condition_variable基础级别,那么那些不需要的功能的客户my_better_condition_variable无论如何都必须为此付费。

这种将最快、最原始的设计放在堆栈底部的理念,目的是在它们之上构建更好/更慢的东西,贯穿整个 C++ 库。在 C++ 库未能遵循这一理念的地方,客户经常(并且正确地)被激怒。

于 2012-06-15T00:35:40.513 回答
6

如果您不想编写循环,则可以使用采用谓词的重载

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

它被定义为等效于循环,因此它就像原始代码一样工作。

于 2012-06-15T00:31:27.150 回答
4

即使您可以这样做,C++11 规范也允许cv.wait()虚假解除阻塞(以考虑具有该行为的平台)。因此,即使没有 vulture 线程(抛开它们是否应该存在的争论),消费者线程也不能指望有一个 cookie 等待,并且仍然需要检查。

于 2012-06-15T00:30:33.390 回答
0

我认为这是不安全的:

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

当您通知另一个等待锁的线程时,您仍然持有锁。所以可能是另一个线程立即唤醒并尝试在释放锁之后调用的析构函数之前获取锁。这意味着另一个线程最终会返回以永远等待。 cv.notify_one()

所以我认为这应该编码为:

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

或者如果您不喜欢手动解锁

void producer_thread() {
    {
        std::unique_lock<std::mutex> lock(m);
        q.push(42);
    }
    cv.notify_one();
} 
于 2015-05-12T12:25:11.240 回答