《C++ 并发编程实战 第二版》:条件变量唤醒丢失与虚假唤醒

《C++ 并发编程实战 第二版》:条件变量唤醒丢失与虚假唤醒

推荐阅读

《C++ 并发编程实战 第二版》学习笔记目录

本文主要是对《C++ 并发编程实战 第二版》第 4 章中条件变量部分做进一步探究,主要内容为使用条件变量时可能会碰到的两个坑:唤醒丢失与虚假唤醒

唤醒丢失

唤醒丢失情况1:缺少条件

例子:来自参考资料2

std::mutex mutex;
std::condition_variable cv;
std::vector<int> vec;

void Consume() {
  std::unique_lock<std::mutex> lock(mutex);
  cv.wait(lock);
  std::cout << "consume " << vec.size() << "\n";
}

void Produce() {
  std::unique_lock<std::mutex> lock(mutex);
  vec.push_back(1);
  cv.notify_all();
  std::cout << "produce \n";
}

int main() {
  std::thread t(Consume);
  t.detach();
  Produce();
  return 0;
}

如果先执行的Produce(),后执行的Consume(),生产者提前生产出了数据,去通知消费者,但是此时消费者线程如果还没有执行到wait语句,即线程还没有处于挂起等待状态,线程没有等待此条件变量上,那通知的信号就丢失了,后面Consume()中才执行wait处于等待状态,但此时生产者已经不会再触发notify,那消费者线程就会始终阻塞下去,出现bug。
---- 程序喵大人

唤醒信号是一次性的且存在丢失可能,因此我们需要根据实际情况决定是否要等待

  • 如果先执行的Produce(),后执行的Consume(),队列中已经有东西了,那么就不用等待了
  • 否则,队列开始的那么还是要等待

因此我们需要给等待加上条件(下面的代码是有问题的!!!)

std::mutex mutex;
std::condition_variable cv;
std::vector<int> vec;

void Consume() {
  std::unique_lock<std::mutex> lock(mutex);
  if (vec.empty()) { // 加入此判断条件,但这样虚假唤醒的问题!!!
      cv.wait(lock);
  }
  std::cout << "consume " << vec.size() << "\n";
}

void Produce() {
  std::unique_lock<std::mutex> lock(mutex);
  vec.push_back(1);
  cv.notify_all();
  std::cout << "produce \n";
}

int main() {
  std::thread t(Consume);
  t.detach();
  Produce();
  return 0;
}

这样就能解决信号丢失的问题,但是使用 if来进行条件判断虚假唤醒的问题,这个在后面虚假唤醒的部分解决。建议暂时跳过唤醒丢失情况 2:没有搭配锁 这部分内容,先去看虚假唤醒这部分内容

唤醒丢失情况2:没有搭配锁

例子:来自参考资料4

class Foo {
    condition_variable cv;
    mutex mtx;
    int k = 0;
public:
    void first(function<void()> printFirst) {
        printFirst();
        k = 1;
        cv.notify_all();    // 通知其他所有在等待唤醒队列中的线程
    }

    void second(function<void()> printSecond) {
        unique_lock<mutex> lock(mtx);   // lock mtx
        cv.wait(lock, [this](){ return k == 1; });  // unlock mtx,并阻塞等待唤醒通知,需要满足 k == 1 才能继续运行
        printSecond();
        k = 2;
        cv.notify_one();    // 随机通知一个(unspecified)在等待唤醒队列中的线程
    }

    void third(function<void()> printThird) {
        unique_lock<mutex> lock(mtx);   // lock mtx
        cv.wait(lock, [this](){ return k == 2; });  // unlock mtx,并阻塞等待唤醒通知,需要满足 k == 2 才能继续运行
        printThird();
    }
};


作者:zintrulcre
链接:https://leetcode.cn/problems/print-in-order/solution/c-hu-chi-suo-tiao-jian-bian-liang-xin-hao-liang-yi/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

上面是力扣多线程题目 1114. 按序打印 的一片题解的条件变量解法部分,评论区中有人指出这个做法存在问题。
考虑下面的情况

在这里插入图片描述
由于在修改 k 的共享内存的时候没有加锁,导致线程 2 检查条件 k == 1 发现结果为 false 然后决定等待的过程中 k 的值发生改变。条件变量开始等待之前 k = 1(检查条件失效) 并且唤醒信号已经被发送(唤醒丢失)

class Foo {
    mutex mtx;
    condition_variable cv;
    int k = 0;
  
public:
    Foo() {
    }

    void first(function<void()> printFirst) {
        // printFirst() outputs "first". Do not change or remove this line.
        lock_guard<mutex> lock(mtx);
        printFirst();
        k = 1;
        cv.notify_all();
    }

    void second(function<void()> printSecond) {
    
        // printSecond() outputs "second". Do not change or remove this line.
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, [this](){ return k == 1;});
        printSecond();
        k = 2;
        cv.notify_one();
        
    }

    void third(function<void()> printThird) {
        
        // printThird() outputs "third". Do not change or remove this line.
        unique_lock<mutex> lock(mtx);
        cv.wait(lock, [this](){ return k == 2;});
        printThird();
    }
};

虚假唤醒

什么是虚假唤醒?

参考资料 5(机翻了一下建议看原文)
当线程从等待已发出信号的条件变量中醒来,却发现它等待的条件未得到满足时,就会发生虚假唤醒。之所以称为虚假,是因为该线程似乎无缘无故地被唤醒了。但是虚假唤醒不会无缘无故发生:它们通常是因为在发出条件变量信号和等待线程最终运行之间,另一个线程运行并更改了条件。线程之间存在竞争条件,典型的结果是有时,在条件变量上唤醒的线程首先运行,赢得竞争,有时它运行第二,失去竞争。
在许多系统上,尤其是多处理器系统上,虚假唤醒的问题更加严重,因为如果有多个线程在条件变量发出信号时等待它,系统可能会决定将它们全部唤醒,将每个signal( )唤醒一个线程视为broadcast( )唤醒所有这些,从而打破了信号和唤醒之间任何可能预期的 1:1 关系。如果有 10 个线程在等待,那么只有一个会获胜,另外 9 个会经历虚假唤醒。
为了让实现在处理操作系统内部的错误条件和竞争时具有灵活性,即使没有发出信号,也可以允许条件变量从等待中返回,尽管目前尚不清楚有多少实现实际上这样做了。在条件变量的 Solaris 实现中,如果进程发出信号,则可能发生虚假唤醒而没有发出条件信号;等待系统调用中止并返回EINTR。条件变量的 Linux p-thread 实现保证它不会那样做。
因为只要有竞争甚至可能在没有竞争或信号的情况下都可能发生虚假唤醒,因此当线程在条件变量上唤醒时,它应该始终检查它所寻求的条件是否得到满足。如果不是,它应该回到条件变量上睡觉,等待另一个机会。

虚假唤醒情况 1:notify_one 但是多线程争抢

例子:来自参考资料1

std::condition_variable cv;
std::mutex mx;

void thread1()
{
    while (true) {
        // do some work ...
        std::unique_lock<std::mutex> lock(mx);
        cv.notify_one();    // wake other thread
    }
}

void thread2()
{
    while (true) {
        std::unique_lock<std::mutex> lock(mx);
        cv.wait(lock);    // might block forever
        // do work ...
    }
}

在这里,如果有其他thread消费者thread1的通知,thread2会永久等待

虚假唤醒情况 2 : 系统原因

有些操作系统为了在处理内部的错误条件和竞争时具有灵活性,即使没有发出信号,也可以允许条件变量从等待中返回。因此下面的代码在某些操作系统会存在问题(也就是唤醒丢失情况 1 当中的改进代码)

linux 系统提供的 pthread 保证不会发生这种情况的虚假唤醒

void Consume() {
  std::unique_lock<std::mutex> lock(mutex);
  if (vec.empty()) { // 加入此判断条件,但这样虚假唤醒的问题!!!
      cv.wait(lock);
  }
  std::cout << "consume " << vec.size() << "\n";
}

void Produce() {
  std::unique_lock<std::mutex> lock(mutex);
  vec.push_back(1);
  cv.notify_all();
  std::cout << "produce \n";
}

我们应当使用 while 而不是 if 来判断条件

void Consume() {
  std::unique_lock<std::mutex> lock(mutex);
  while (vec.empty()) { // 使用 while 来判断条件
      cv.wait(lock);
  }
  std::cout << "consume " << vec.size() << "\n";
}

对于 C++ 我们可以直接将条件通过 lambda 的方式传递给条件变量,C++ 内部会自动使用 while进行判断

 while (vec.empty()) { // 使用 while 来判断条件
     cv.wait(lock);
 }
// 和下面的等效
cv.wait(lock, [](){ return vec.empty();} );

小结

  • 条件变量必须搭配互斥锁使用
  • 尽可能使用 C++ 提供的带条件的条件变量形式
  • 如果直接使用系统底层的条件变量,要注意唤醒丢失和虚假唤醒这两个坑,最好能进行封装后再使用

参考资料

  1. CppCoreGuidelines CP.42 dont-wait-without-a-condition 唤醒丢失情况 1
  2. 使用条件变量的坑你知道吗 比较全面的介绍,可以看看
  3. 虚假唤醒 对参考资料1的补充
  4. 力扣多线程 1114. 按序打印题解 条件变量的解法有唤醒丢失的问题
  5. 维基百科 虚假唤醒
  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 对于需要学习并发编程的人员来说,《Java并发编程实战》是一本非常经典的书籍。现在这本书已经改版,发布了第二版。想要下载《Java并发编程实战 第二版》PDF文档,可以在网上进行搜索。为了保证下载到正版的电子书,最好选择合法的网站进行下载。如果不清楚哪些是合法的下载网站,可以参考一些知名的IT科技网站,如CSDN、博客园,或直接到出版社官网下载。在学习并发编程的过程中,需要深入理解书中的并发编程基础知识,包括线程安全、锁、原子性等概念,学习掌握Java并发包和工具的使用。同时,在实际应用中要注意并发问题的处理,避免产生死锁、性能问题以及其他并发编程常见的问题。《Java并发编程实战 第二版》不仅可以帮助读者深入理解并发编程,还能为读者提供丰富的例子和实战经验,是一本非常实用的并发编程技术书籍。 ### 回答2: C++ 并发编程实战是一本关于多线程和并发编程的经典著作,本书已经推出了第二版。该书介绍了多线程及其相关技术,包括线程之间的同步、互斥、锁、原子操作等。此外,本书还涵盖了异步编程、并发数据结构、并发算法等高级主题。 如果你需要深入了解 C++ 并发编程,这本书就是你的一个不错的选择。该书主要分为三部分:基础知识、高级话题以及案例研究。第一部分主要介绍了多线程编程的概念、理论和实践,讲述了如何并发编程、如何避免竞争条件以及如何确保线程之间的同步。第二部分涵盖了一些高级话题,如锁、原子操作、并发算法等内容。最后一部分则是案例研究,该部分通过一个完整的示例来演示并发编程的各个方面。 总之,本书是一本系统全面的 C++ 并发编程教材,适合具备 C++ 编程基础的读者阅读。如果你想深入理解多线程和并发编程,这本书是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值