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

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

本文关键字:通知 线程 condition std variable 我们 C++11      更新时间:2023-10-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()之前实际锁定互斥锁的好方法(请参阅向条件变量发出信号(pthreads))。

我很确定 C++11 没有任何这个成语的标准实现。这有什么历史原因(只是 pthreads 没有这样做吗?然后为什么会这样)?是否有技术原因导致一个冒险的C++程序员无法在标准 C++11 中实现这个成语,称之为可能my_better_condition_variable

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

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

然而,pthreads 也为这种行为提供了很好的理由。 从开放组基本规格:

其效果是多个线程可以从其调用 pthread_cond_wait() 或 pthread_cond_timedwait() 作为 1 的结果 调用 pthread_cond_signal()。这种效应称为"虚假的" 醒醒"。请注意,这种情况是自我纠正的,因为数字 如此觉醒的线是有限的;例如,下一个 在上述事件序列之后调用 pthread_cond_wait() 的线程 块。

虽然这个问题可以解决,但效率的损失 很少发生的边缘疾病是不可接受的,尤其是 鉴于必须检查与条件相关的谓词 无论如何都是可变的。纠正此问题将不必要地减少 此基本构建块中所有人的并发程度 更高级别的同步操作。

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

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

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

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

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

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

即使你可以这样做,C++11规范也允许cv.wait()虚假地解锁(以考虑具有该行为的平台)。因此,即使没有秃鹫线程(撇开关于它们是否应该存在的争论),消费者线程也不能指望有一个cookie在等待,仍然必须检查。

我认为这不安全:

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