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

16
我是一名有用的助手,能够翻译文本。
我正在学习关于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

现在,秃鹫线程无法介入,因为互斥锁从"I MADE YOU A COOKIE"一直保持锁定状态到"YUM"。另外,如果notify()需要传递一个锁,那么这是确保人们在调用notify()之前实际上锁定了互斥锁的好方法(参见Signalling a condition variable (pthreads))。
我相信C++11没有这种惯用语的任何标准实现。这背后的历史原因是什么(仅仅是pthread没有做吗?然后为什么是那样的)?有没有技术原因,一个冒险的C++编码者不能在标准C++11中实现这种惯用语,称它为my_better_condition_variable?
我还有一种模糊的感觉,也许我正在重新发明信号量,但我不记得学校里的东西是否准确。

3
如果你不想让某个线程吃掉饼干,那你为什么要运行它呢? - R. Martinho Fernandes
2
好的,我的问题语气有点偏向反鸟类了。 :)你看,消费者正在做一个美好的梦。他愿意为了饼干和牛奶而被叫醒,但如果他醒来发现没有饼干,他会很失望。所以生产者把饼干喂给秃鹫是可以的,但生产者不可以摇醒消费者,大声喊“饼干时间!”然后说“哦,糟糕,我在摇你的时候秃鹫吃掉了你的饼干。”那不好,生产者。那不好。 - Quuxplusone
让我们在聊天室里继续这个讨论 - Quuxplusone
2
+1 对于有关秃鹫的可爱故事 :)。 - Red XIII
在 'producer_thread()' 中,您在仍然持有锁时调用 'notify_one()'。这可能会有问题吗? - Michael Lehn
显示剩余5条评论
4个回答

10
最终的答案是因为pthread没有这样做。C++是一种封装操作系统功能的语言,而不是操作系统或平台。所以它包装了像Linux、Unix和Windows等操作系统的现有功能。
然而,pthreads也有一个很好的理由来解决这个问题。来自开放组基本规范:
效果是,多个线程可以从其调用pthread_cond_wait()或pthread_cond_timedwait()返回,因为一个调用pthread_cond_signal()。这种效果称为"虚假唤醒"。请注意,情况是自我纠正的,因为被唤醒的线程数是有限的;例如,在上述事件序列之后调用pthread_cond_wait()的下一个线程将阻塞。
虽然这个问题可以解决,但是由于极少发生的边缘条件而损失效率是不可接受的,特别是考虑到必须检查与条件变量相关联的谓词。纠正这个问题会不必要地减少所有更高级同步操作的这个基本构建块中并发度。
允许虚假唤醒的另一个好处是它强制应用程序在条件等待周围编写谓词测试循环。这也使应用程序容忍在应用程序的其他部分可能编码的相同条件变量上的多余条件广播或信号。因此,IEEE Std 1003.1-2001明确记录了虚假唤醒可能会发生。
所以基本上的说法是,你可以在pthread条件变量(或std::condition_variable)的基础上相当容易地构建my_better_condition_variable,并且不会有性能惩罚。然而,如果将my_better_condition_variable放在基本级别,那么那些不需要my_better_condition_variable功能的客户端也必须为其支付代价。

这种“自下而上”的设计哲学贯穿于 C++ 标准库,即使用最快、最原始的设计作为底层,以便更好、更慢的东西可以在其之上构建。如果 C++ 标准库不遵循这种哲学,客户通常会(并且应该)感到烦恼。


R. Martinho Fernandes和我在聊天中进行了一次愉快而长久的讨论,所以我没有更多未回答的问题。 pthreads做出了决定,即您不能在一个线程中锁定互斥锁并在另一个线程中解锁它;因此,在我的示例中将无法将pthread互斥锁上的锁从生产者传递到消费者(而std::mutex是基于pthreads模拟的)。构建my_better_condition_variable因此需要比我想象的更多的工作,因为您首先必须构建my_unsafer_mutex。这将是一个完整的线程库,并且与C++11库不兼容。 - Quuxplusone
1
“所以基本上这个说法是,你可以轻松地构建……而且没有性能惩罚。” 然后,“然而,如果我们放置……他们仍然必须为此付出代价。” 如果没有性能惩罚,那么他们究竟要付费用来获得什么呢? - ildjarn
1
@ildjarn:如 POSIX 解释所述,循环等待的必要性在于 wait 可能会出现虚假唤醒(即未被信号通知也会返回)。声称实现一个可以在 wait 中产生虚假唤醒的条件变量比实现不产生虚假唤醒的条件变量要便宜得多。而且大多数条件变量的客户端都会忽略虚假唤醒。因此那些客户端不应该为保证不出现虚假唤醒的条件变量付费。 - Howard Hinnant
@Howard:令人困惑的是“没有性能惩罚”这个短语。 - ildjarn

7
如果你不想编写循环,可以使用带有谓词的重载函数
cv.wait(lock, [&q]{ return !q.is_empty(); });

它被定义为与循环相等,因此它的工作方式与原始代码完全相同。


1
(仅供记录)我对这个解决方案的反对意见是它仍然有一个轮询循环,隐藏在“等待”实现内部。如果我的代码要循环,我实际上更喜欢显式的“while”循环。 - Quuxplusone
2
@Quuxplusone:在C++标准中,“等价于”与“基于实现”的概念相差甚远。“等价于”只是解释了期望的语义/结果,但这并不意味着在此处以任何方式存在文字轮询循环。 - ildjarn

4
即使你能够这样做,C++11规范允许cv.wait()在没有cookie等待的情况下突然解除阻塞(以适应具有该行为的平台)。因此,即使没有任何其他线程(暂不考虑它们是否应该存在),消费者线程也不能指望有一个cookie在等待,并且仍然需要进行检查。

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

1
这是真的吗? - dani
1
根据http://en.cppreference.com/w/cpp/thread/condition_variable的说法,通知时不需要持有锁。但是他们只是说“不需要”-所以这至少不是不正确的。 我曾经看到过helgrind(valgrind)报告,如果您进行通知,则会丢失锁定。 - Martin Gerhardy

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