【Linux】条件变量及生产者消费者模型

为什么要将这两者放在一起进行呢?
主要是因为生产消费与条件变量关系密切,正好相辅相成。

目录

  • 条件变量:
    • 条件变量的引出:
    • 条件变量的解释与接口:
    • 测试代码:
  • 生产者消费者模型:
    • 概念:
    • 代码实现Blocking Queue:
    • 完整代码:
    • 测试结果:
    • 一些问题:

条件变量:

条件变量的引出:

我们假设有一个自习室,这个自习室每次只能有一个人进入(使用挂在自习室门前钥匙),其他的人都要排队(排队之前都去试图找钥匙)。

今天小A起了个大早去拿钥匙,一直学到中午,此时饿得受不了了,于是想出去吃饭,他就走到门前打算归还钥匙,可是刚挂到门前就后悔了,因为她不想排队那么久才能继续学习,于是又拿着钥匙将门打开,但是又很饿,就这样循环往复,自己没有得到知识,外面的人也只能干排队等待。

与之对应:
自习室就相当于临界资源,钥匙就是锁,当其中一个线程拿到锁就可以访问临界资源,其他的线程都陷入阻塞状态。
由于线程A继续访问临界资源已经没有意义,但是竞争锁的能力很强,因为距离自己很近,造成了其他线程饥饿问题

虽然线程A这样做合法,但是不道德。
我们此时需要制定规则,保证过程具有一定顺序性,也就是同步。
而条件变量就是确保这个顺序性的!

条件变量的解释与接口:

我们知道了条件变量的作用,但是还是并不清楚条件变量是什么。

我们再来一个例子进行解释。

假设有一个游戏,一个蒙眼拿盘子中的苹果,一个睁眼将苹果放入盘子。

在这里插入图片描述

现在我们就可以对应一下了。
铃铛就是条件变量,他是一个队列,可以让线程进行等待
他的唤醒有两种策略,全部唤醒与单个唤醒。

我们也就可以对应一下条件变量的接口了。
有些接口与锁很类似。
在这里插入图片描述
其中init是条件变量初始化的函数,与锁一致。
signal与broadcast就是通知接口,signal是一次通知一个,boardcast就是全部通知
wait就是去铃铛下等待,也就是去条件变量下等待。
timedwait我们不管。
desory就是销毁条件。

关于这里还有一个细节,为什么条件变量要把锁业传入?
后面会进行解释。

测试代码:

我们目前可以浅浅的使用一下条件变量,熟悉一下接口。

我们现在要实现一个场景,在这里插入图片描述
主要逻辑是先创建一批线程并进行管理,每个创建好的线程都去执行各自的任务,任务是个死循环,每次进入在条件变量下等待被唤醒。

#include <iostream>
#include <pthread.h>
#include <unistd.h>const int N = 5;pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;void *Run(void *args)
{while (true){pthread_mutex_lock(&g_mutex);pthread_cond_wait(&g_cond, &g_mutex);std::cout << "i am new thread-" << reinterpret_cast<long long>(args) << std::endl;pthread_mutex_unlock(&g_mutex);}
}int main()
{pthread_t thds[N];for (int i = 0; i < N; i++){pthread_create(&thds[i], nullptr, Run, reinterpret_cast<void*>(i));}// 唤醒while (true){sleep(1);pthread_cond_signal(&g_cond);}for (auto &val : thds){pthread_join(val, nullptr);}return 0;
}

现象:
在这里插入图片描述

生产者消费者模型:

概念:

条件变量暂时说到这里,我们先来谈一谈这个模型。

我们依旧是拿具体场景进行解释:
这是最简单的一个图示
在这里插入图片描述
实际上,我们的生产者代表的就是供货商,超市是一块缓存,顾客就是消费者。

这个模型有3大优点:

  1. 协调忙闲不均:
    当生产慢时而消费又多,我们就可以提前生产一批产品到超市;
    当生产快时而消费又慢,我们就可以提前让消费者来到超市;
  2. 效率高:
    你在买东西时,供应商在生产,达到了消费生产并发
    你在处理你买的东西时,供应商把商品放入超市,达到放入处理并发
  3. 解耦:
    生产者与消费者互不影响:比如一共有10个供货商,其中一个倒闭了,并不影响你买商品;当你吃泡面撒掉了,也不会影响供应商供货。

高度提炼一下可以简记为:1 2 3 原则。
1:一个交易场所(一段内存空间)
2:两种角色(生产角色,消费角色)
3:三种关系(生产与生产,消费与消费,生产与消费)
其中这三种关系分别为互斥,互斥,互斥&&同步。

相信前两个都很好理解,第三个呢?

比如供应商生产好商品还没录入数据就肯定不能被顾客拿走,这就是互斥。
这就像你在写数据,还没写完就被读走了一样,他们之间也是需要互斥的。
同样,如果超市没货了,我们要要依靠超市通知供应商,等有货了在反过来通知顾客。这就是同步。

代码实现Blocking Queue:

为什么写单对单?因为他简单!为什么不吃牛,因为他善!在这里插入图片描述
不过为什么单对单简单,因为我们的1 2 3原则中有三个关系,生产与生产,消费与消费,生产与消费,单对单就不需要考虑前两种,自然而言的简单了许多。
这里我们要解决一个历史问题:为什么wait要传入锁?

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。
其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;
当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
在这里插入图片描述

那么我们现在就要有一个大概的轮廓:

生产者一直写,消费者一直读即可,很基础的框架,有了框架我们就知道如何下手实现主要功能了。

#include "BlockingQueue.hpp"void *Consumer(void *args)
{BlockingQueue *bq = static_cast<BlockingQueue*>(args);while (true){// 接收数据bp.Pop();// 处理数据}
}void *Producer(void *args)
{BlockingQueue *bq = static_cast<BlockingQueue*>(args);while (true){// 构建数据bp.Push();}
}int main()
{BlockingQueue pc;pthread_t t1, t2;pthread_create(&t1, nullptr, Producer, static_cast<void*>(&pc));pthread_create(&t2, nullptr, Consumer, static_cast<void*>(&pc));pthread_join(t1, nullptr);pthread_join(t2, nullptr);return 0;
}

这是我们的阻塞队列,里面有对于一些点的详细注释,
比如cond为什么要传入锁?唤醒在哪个位置进行唤醒?
而又因为我们也不确定用户使用的是什么类型,设计为模板即可。

#pragma once#include <iostream>
#include <pthread.h>
#include <queue>
#include <unistd.h>template <class T>
class BlockingQueue
{
public:BlockingQueue(int cap = 5) : _cap(cap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond_p, nullptr);pthread_cond_init(&_cond_c, nullptr);}void Push(const T &val){pthread_mutex_lock(&_mutex);if (IsFull()){pthread_cond_wait(&_cond_p, &_mutex);// 所以这里的wait为什么要传锁就一目了然了// 因为如果你带着锁去wait,那么别的线程只会一直阻塞,不会拿到锁// 所以wait还要进行解锁,等被唤醒在继续抢锁。}// 出来了就代表肯定不是满的,此时push数据即可。_q.push(val);pthread_mutex_unlock(&_mutex);pthread_cond_signal(&_cond_c);// 注意这里的唤醒放在解锁的上或下都可以,当放在下时,我们还是以单对单为例// 假设我们此时队列中还没有数据,消费者在wait中解锁阻塞,生产者生产完数据进行解锁再唤醒消费者,// 而这时消费者会和生产者再次抢锁,因为消费者会从wait中被唤醒需要抢锁,而生产者在lock中抢锁。// 如果消费者抢到了就罢了,直接进行写入数据;但是如果生产者抢到了,那就继续生产数据,// 我们假设生产者一直将队列写满,那么他就会在wait中进行阻塞,让消费者写入数据在解锁唤醒。// 同理,实际上因为锁的钳制,在解锁前或后唤醒都是可以的}void Pop(T *val){pthread_mutex_lock(&_mutex);if (IsEmpty()){pthread_cond_wait(&_cond_c, &_mutex);}// 出来了就代表此时数据肯定不为空,可以写给消费者了。*val = _q.front();_q.pop();pthread_mutex_unlock(&_mutex);pthread_cond_signal(&_cond_p);}~BlockingQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond_p);pthread_cond_destroy(&_cond_c);}private:bool IsFull(){return _cap == _q.size();}bool IsEmpty(){return _q.empty();}private:int _cap;std::queue<T> _q;pthread_mutex_t _mutex;pthread_cond_t _cond_p;pthread_cond_t _cond_c;
};

但是这里还有一个细节,我们判断空或满时使用了if,这在某些情况下是会出问题的。

比如当前队列中没有数据。
有一个生产者,两个消费者。
当生产者刚刚生产完,进行了broadcast唤醒,导致两个消费者线程都被唤醒我们假设生产者此时不会抢到锁,所以目前只会有一个消费者线程抢到锁,当其中一个A抢到后,进行写入数据并pop,这都是没有问题的,但是如果A解锁后被另一个消费者B抢到,那么B又会继续写入并进行pop。
问题就出现了,因为唯一的一个数据已经被A拿走了,此时就会出现err,所以我们要对if进行一下修改,改为while,这样就避免了因为B抢到锁而继续pop造成的err。

完整代码:


```cpp
#pragma once#include <iostream>
#include <pthread.h>
#include <queue>
#include <unistd.h>template <class T>
class BlockingQueue
{
public:BlockingQueue(int cap = 5) : _cap(cap){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond_p, nullptr);pthread_cond_init(&_cond_c, nullptr);}void Push(const T &val){pthread_mutex_lock(&_mutex);while (IsFull()){pthread_cond_wait(&_cond_p, &_mutex);// 所以这里的wait为什么要传锁就一目了然了// 因为如果你带着锁去wait,那么别的线程只会一直阻塞,不会拿到锁// 所以wait还要进行解锁,等被唤醒在继续抢锁。}// 出来了就代表肯定不是满的,此时push数据即可。_q.push(val);pthread_mutex_unlock(&_mutex);pthread_cond_signal(&_cond_c);// 注意这里的唤醒放在解锁的上或下都可以,当放在下时,我们还是以单对单为例// 假设我们此时队列中还没有数据,消费者在wait中解锁阻塞,生产者生产完数据进行解锁再唤醒消费者,// 而这时消费者会和生产者再次抢锁,因为消费者会从wait中被唤醒需要抢锁,而生产者在lock中抢锁。// 如果消费者抢到了就罢了,直接进行写入数据;但是如果生产者抢到了,那就继续生产数据,// 我们假设生产者一直将队列写满,那么他就会在wait中进行阻塞,让消费者写入数据在解锁唤醒。// 同理,实际上因为锁的钳制,在解锁前或后唤醒都是可以的}void Pop(T *val){pthread_mutex_lock(&_mutex);while (IsEmpty()){pthread_cond_wait(&_cond_c, &_mutex);}// 出来了就代表此时数据肯定不为空,可以写给消费者了。*val = _q.front();_q.pop();pthread_mutex_unlock(&_mutex);pthread_cond_signal(&_cond_p);}~BlockingQueue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond_p);pthread_cond_destroy(&_cond_c);}private:bool IsFull(){return _cap == _q.size();}bool IsEmpty(){return _q.empty();}private:int _cap;std::queue<T> _q;pthread_mutex_t _mutex;pthread_cond_t _cond_p;pthread_cond_t _cond_c;
};

main函数代码:

#include "BlockingQueue.hpp"#include <random>void *Consumer(void *args)
{BlockingQueue<int> *bq = static_cast<BlockingQueue<int>*>(args);while (true){sleep(2);// 接收数据int val;bq->Pop(&val);// 处理数据std::cout << "Consumer receive a data: " << val << std::endl;}
}void *Producer(void *args)
{BlockingQueue<int> *bq = static_cast<BlockingQueue<int>*>(args);while (true){// 构建数据int val = rand() % 10 + 1;// [1, 10]bq->Push(val);std::cout << "Producer produce a data: " << val << std::endl;}
}int main()
{srand(time(nullptr));BlockingQueue<int> pc;pthread_t t1, t2;pthread_create(&t1, nullptr, Producer, static_cast<void*>(&pc));pthread_create(&t2, nullptr, Consumer, static_cast<void*>(&pc));pthread_join(t1, nullptr);pthread_join(t2, nullptr);return 0;
}

测试结果:

在这里插入图片描述
观察到生产者生产顺序为1 9 6 6 6…而消费者也正好是1 9 6 6 6…

注意:我们的生产者消费者不仅仅只能传递最简单的内置类型,也可以进行传递自定义类型!可调用对象也是可以的!

一些问题:

我们的多生产多消费不需要修改代码,因为锁已经帮我们搞定了生产与生产,消费与消费之间的关系。

为什么明明一次只能有一个线程访问队列,但还是说生产消费模型效率高?
因为我们不能只关注生产与消费在缓存的时间!
我们的生产任务或数据需要时间,处理任务或数据需要时间。
也就是说:当其中一个生产者在放任务,其他的生产者在生产任务;
其中一个消费者在拿任务,其他消费者在处理任务。
这种并发才让我们的效率变高!

为什么要在锁之后再进行条件变量wait?
这是因为我们push或pop之前一定会临街资源,临界资源一定是被锁保护起来的,所以设计者才会这样设计接口,我们也才要这样使用。

完~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/1486759.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

raise JSONDecodeError(“Expecting value”, s, err.value) from None

raise JSONDecodeError(“Expecting value”, s, err.value) from None 目录 raise JSONDecodeError(“Expecting value”, s, err.value) from None 【常见模块错误】 【解决方案】 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是…

unity文字||图片模糊

一.文字模糊 1、增大字体大小后等比缩放 快捷键R 2、更改字体渲染模式 二.图片模糊 1、更改过滤模式 2、更改格式或者压缩 3、如果只是图片边缘看不清&#xff0c;可以增加canvas/图片的每单位参考像素

41-50题矩阵和字符串 在Java中,将大写字符转换为小写字符的方法主要有以下几种:

20240723 一、数组最后几个和字符串的两个448. 找到所有数组中消失的数字&#xff08;和645. 错误的集合差不多&#xff09;283. 移动零118. 杨辉三角119. 杨辉三角 II661. 图片平滑器&#xff08;没看懂&#xff09;598. 区间加法 II566. 重塑矩阵303. 区域和检索 - 数组不可变…

【计算机网络】三次握手、四次挥手

问&#xff1a;三次握手 四次挥手 TCP 连接过程是 3 次握手&#xff0c;终止过程是 4 次挥手 3次握手 第一步&#xff1a;客户端向服务器发送一个带有 SYN&#xff08;同步&#xff09;标志的包&#xff0c;指示客户端要建立连接。 第二步&#xff1a;服务器收到客户端的请求…

麒麟V10安装nginx、mysql报错缺少包:error while loading shared libraries libssl.so.10

背景 启动nginx报错&#xff1a;error while loading shared libraries libssl.so.10 解决 查看nginx启动文件所依赖的动态链接库&#xff08;即共享库或动态库&#xff09; ldd nginx-1.22.1/sbin/nginx离线安装compat-openssl10包 将依赖包麒麟v10安装openssl10依赖包上…

Vuex数据持久化实现

版本&#xff1a;vue 3.4.29 vuex4.1.0 1. 出现的问题 当我使用 vuex 作为状态管理组件来存储用户的一些信息之后&#xff0c;发现从/login 页面跳转到/home 界面后拿不到vuex信息。 之后查阅资料了解&#xff0c;当切换路由后&#xff0c;vue 会重新渲染&#xff0c;而vuex 也…

为什么样本方差(sample variance)的分母是 n-1?

样本均值与样本方差的定义 首先来看一下均值&#xff0c;方差&#xff0c;样本均值与样本方差的定义 总体均值的定义&#xff1a; μ 1 n ∑ i 1 n X i \mu\frac{1}{n}\sum_{i1}^{n} X_i μn1​i1∑n​Xi​ 也就是将总体中所有的样本值加总除以个数&#xff0c;也可以叫做总…

运维团队如何借助分布式部署提升监控效率与可靠性

随着企业IT基础设施的日益复杂和分布式架构的广泛应用&#xff0c;传统的监控解决方案已经难以满足现代运维团队的需求。在这样的背景下&#xff0c;分布式部署作为一种新型的监控架构&#xff0c;以其灵活性、可扩展性和高可用性&#xff0c;成为了运维团队提升监控效率与可靠…

centos系统mysql数据库差异备份与恢复

文章目录 差异备份mysql数据一、 安装 Percona XtraBackup数据库中创建一些数据三、创建全备份四、创建差异备份1. 在数据库中添加数据&#xff0c;让数据发生一些改变2. 创建第一个差异备份3. 数据库中再次添加一些数据4. 创建第二个差异备份 五、模拟数据丢失&#xff0c;删库…

nest学习笔记(一)

介绍 nest是一个用于构建高效&#xff0c;可拓展的nodejs服务端应用程序的框架&#xff0c;它使用渐进式javascript&#xff0c;使用Typescript构建并且完全支持Typescript&#xff0c;而且运行开发者使用javascript编写代码&#xff0c;提供了OOP、FP、FRP nest的底层是基于…

Kolla-Ansible的确是不支持CentOS-Stream系列产品了

看着OpenStack最新的 C 版本出来一段时间了&#xff0c;想尝个鲜、用Kolla-Ansible进行容器化部署&#xff0c;结果嘛。。。 根据实验结果&#xff0c;自OpenStack Bobcat版本开始&#xff0c;Kolla-Ansible就适合在CentOS系列产品上部署了&#xff0c;通过对 Bobcat和Caracal…

springcloud接入skywalking作为应用监控

下载安装包 需要下载SkyWalking APM 和 Java Agent 链接: skywalking 安装 下载JDK17&#xff08;可不配置环境变量&#xff09; 目前skywalking 9.0及以上版本基本都不支持JDK8&#xff0c;需要JDK11-21&#xff0c;具体版本要求在官网查看。 我这里使用的是skywalking9.…

每日好题(2)

#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main(void) {int arr[6] { 1,2,3,4,5,6 };char* p arr;int sz sizeof(arr) / sizeof(arr[0]);for (int a 0; a < sz; a){printf("%d\n", *p);p 4;}return 0; }这串代码遍历打印数组的结果是没…

BGP选路之Preferred value

原理概述 当一台BGP路由器中存在多条去往同一目标网络的BGP路由时&#xff0c;BGP协议会对这些BGP路由的属性进行比较&#xff0c;以确定去往该目标网络的最优BGP路由&#xff0c;然后将该最优BGP路由与去往同一目标网络的其他协议路由进行比较&#xff0c;从而决定是否将该最优…

小程序多排数据横向滚动实现

如何实现多排数据滚动效果 swiper 外部容器 swiper-item 每一页的数据 因为现在有多排数据,现在在swiper-item 中需要循环一个数组 初版 <template><view><view class"container"><view class"swiper-box"><swiper class&qu…

操作系统——笔记(1)

操作系统是管理计算机硬件资源&#xff0c;控制其他程序运行并为用户提供交互操作界面的系统软件的集合&#xff0c;控制和管理着整个计算机系统的硬件和软件资源&#xff0c;是最基本的系统软件。 常见的操作系统&#xff1a;ios、windows、Linux。 计算机系统的结构层次&am…

“论软件测试中缺陷管理及其应用”写作框架,软考高级论文,系统架构设计师论文

原创范文 软件缺陷指的是计算机软件或程序中存在的某种破坏正常运行能力的问题、错误&#xff0c;或者隐藏的功能缺陷。缺陷的存在会导致软件产品在某种程度上不能满足用户的需要。在目前的软件开发过程中&#xff0c;缺陷是不可避免的。软件测试是发现缺陷的主要手段&#xf…

计算机网络基础:2.TCP/IP模型中的各层协议、IP地址

一、TCP/IP模型中的各层协议 接着第一篇餐厅运营的例子来解释一下TCP/IP五层模型中的每一层协议&#xff1a; 1. 应用层&#xff08;餐饮一体机&#xff09; 在TCP/IP模型中&#xff0c;应用层直接与用户交互&#xff0c;提供网络服务。这一层将OSI模型的应用层&#xff08;点…

colab进行keras入门随机数和标签的一点思考,例如shape和Dense等

keras官方中文文档 pip install kerasfrom keras import layers from keras import modelsmodel.add(layers.Dense(32,activationrelu,input_shape(100,)))# 添加多个Dense层 model.add(layers.Dense(10,activationsoftmax)) model.compile(optimizerrmsprop,losscategorical_…

pikachu之sql lnjet 字符型注入

先测试一下闭合 注释符号&#xff1a;-- 注释符号可以忽略其后的内容&#xff0c;使得后续的原始查询内容不会影响我们注入的SQL代码。 条件测试&#xff1a;通过and 11和and 12分别测试真假条件&#xff0c;可以判断输入是否成功闭合&#xff0c;并且可以检测注入是否成功。 …