C++多线程快速入门(三):生产者消费者模型与条件变量使用

互斥锁完成

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>

std::deque<int> q;
std::mutex mtx;

static void produce(int val) {
    
    
    while(val--) {
    
    
        std::unique_lock<std::mutex> guard(mtx);
        q.push_front(val);
        mtx.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}
static void consumer() {
    
    
    int data = INT_MAX;
    while(data != 0) {
    
    
        std::unique_lock<std::mutex> guard(mtx);
        if (!q.empty()) {
    
    
            data = q.back();
            q.pop_back();
            std::cout << data << std::endl;
            mtx.unlock();
        } else  {
    
    
			mtx.unlock();
		}
    }
}
void test() {
    
    
    std::thread t1(produce,3);
    std::thread t2(consumer);
    t1.join();
    t2.join();
}

int main() {
    
    
    test();
    return 0;
}

效果如下:

9
8
7
6
5
4
3
2
1
0
Process finished with exit code 1

produce在生产过程中,std::this_thread::sleep_for (std::chrono::seconds(1));表示延时1s,所以生产过程很慢。
consumer存在着一个while循环,只有在接收到表示结束的数据的时候,才会停止,每次循环内部,都是先加锁,判断队列不空,然后就取出一个数,最后解锁。这样其实做了很多无用功,并且CPU占用率很高
可以在consumer内部也加一个小延时,在一次判断后,如果发现队列是空的,那就惩罚一下自己,延时一下,减少CPU的占用率。

static void consumer() {
    
    
    int data = INT_MAX;
    while(data != 0) {
    
    
        std::unique_lock<std::mutex> guard(mtx);
        if (!q.empty()) {
    
    
            data = q.back();
            q.pop_back();
            std::cout << data << std::endl;
            mtx.unlock();
        } else {
    
    
            mtx.unlock();
            std::this_thread::sleep_for(std::chrono::milliseconds(500));
        }
    }
}

条件变量改进模型

c++11提供了#include <condition_variable>头文件,std::condition_variable可以和std::mutex结合一起使用,其中有两个重要的接口,notify_one()wait()
wait()可以让线程陷入休眠状态,在消费者生产者模型中,如果生产者发现队列中没有东西,就可以让自己休眠.notify_one()就是唤醒处于wait中的其中一个条件变量.
那什么时刻使用notify_one()比较好呢,当然是在生产者往队列中放数据的时候了,队列中有数据,就可以赶紧叫醒等待中的线程起来干活了。
下面是主要修改代码:

std::condition_variable cond;

static void produce(int val) {
    
    
    while(val--) {
    
    
        std::unique_lock<std::mutex> guard(mtx);
        q.push_front(val);
        mtx.unlock();
        cond.notify_one();  // 提醒一个waiting的线程
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}
static void consumer() {
    
    
    int data = INT_MAX;
    while(data != 0) {
    
    
        std::unique_lock<std::mutex> guard(mtx);
        // 如果队列为空,就一直等直到被notify_one唤醒
        while(q.empty())
            cond.wait(guard);
        data = q.back();
        q.pop_back();
        mtx.unlock();
        std::cout << data << std::endl;
    }
}

此时CPU的占用率也很低,因为在消费者端,队列为空时,将控制权交给了cpu,直到被唤醒。
需要注意的是在判断队列是否为空的时候,使用的是while(q.empty()),而不是if(q.empty())
这是因为wait()从阻塞到返回,不一定就是由于notify_one()函数造成的,还有可能由于系统的不确定原因唤醒(可能和条件变量的实现机制有关),这个的时机和频率都是不确定的,被称作伪唤醒,如果在错误的时候被唤醒了,执行后面的语句就会错误,所以需要再次判断队列是否为空,如果还是为空,就继续wait()阻塞。
在管理互斥锁的时候,使用的是std::unique_lock而不是std::lock_guard,在上一篇笔记C++多线程快速入门(二)共享数据同步以及数据竞争中,谈到过ock_guard没有lock和unlock接口,而unique_lock提供了。这里的话也是由于此点原因。因为在wait()函数之前,使用互斥锁保护了,如果wait的时候什么都没做,岂不是一直持有互斥锁?那生产者也会一直卡住,不能够将数据放入队列中了。所以,wait()函数会先调用互斥锁的unlock()函数,然后再将自己睡眠,在被唤醒后,又会继续持有锁,保护后面的队列操作。
另外除了notify_one()函数,c++还提供了notify_all()函数,可以同时唤醒所有处于wait状态的条件变量。

参考

https://blog.csdn.net/qq_43145072/article/details/103732176

往期内容回顾

C++多线程快速入门(二)共享数据同步以及数据竞争
C++多线程快速入门(一):基本&常用操作

おすすめ

転載: blog.csdn.net/qq_42604176/article/details/120914038
おすすめ