信号量与生产者消费者问题

2023-05-16

生产者—消费者问题

        生产者—消费者题型在各类考试(考研、程序员证书、程序员面试笔试、期末考试)很常见,原因之一是生产者—消费者题型在实际的并发程序(多进程、多线程)设计中很常见;之二是这种题型综合性较好,涉及进程合作、互斥,有时还涉及死锁的避免。生产者—消费者题型可以全面考核你对并发程序的理解和设计能力。

        生产者—消费者题型最基本的是有界缓冲区的生产者消费者问题和无界缓冲区的生产者消费者问题,对这两个问题的解我们应该掌握其解决方案。

        对于有界缓冲区的生产者—消费者问题,两个进程共享一个公共的固定大小的缓冲区。其中一个是生产者,将信息放入缓冲区;另一个是消费者,从缓冲区中取出信息(也可以把这个问题一般化为m个生产者和n个消费者问题,但是我们只讨论一个生产者和一个消费者的情况,这样可以简化解决方案)。

 

        问题在于当缓冲区已满,而此时生产者还想向其中放入一个新的数据项的情况,其解决办法是让生产者睡眠,待消费者从缓冲区中取出一个或多个数据项时再唤醒它。同样地,当消费者试图从缓冲区取数据而发现缓冲区为空时,消费者就睡眠,直到生产者向其中放入一些数据时再将其唤醒。

        这个方法听起来很简单,为了跟踪缓冲区中的数据项数,我们需要一个变量count。如果缓冲区最多存放N个数据项,则生产者代码将首先检查count是否达到N,若是,则生产者睡眠,否则生产者向缓冲区最多存放N个数据项,则生产者代码将首先检查count是否达到N,若是,则生产者睡眠;否则生产者向缓冲区放入一个数据项并增量count的值。

        消费者的代码与此类似:首先测试count是否为0,若是,则睡眠;否则从中取出一个数据项并递减count的值。每个进程同时也检测另一个进程是否应该被唤醒,若是则唤醒之。生产者消费者的代码如下:

#define N 100
int count = 0;
void producer(void)
{
	int item;
	while(TRUE)
	{
		item = produce_item();
		if(count == N)					//如果缓冲区满就休眠
		sleep();
		insert_item(item);
		count = count + 1;				//缓冲区数据项计数加1
		if(count == 1)
		wakeup(consumer);
	}
}

void consumer(void)
{
	int item;
	while(TRUE)
	{
		if(count == 0)				//如果缓冲区空就休眠
			sleep();
		item = remove_item();
		count = count - 1;			//缓冲区数据项计数减1
		if(count == N - 1)
			wakeup(producer);
		consume_item(item);
	}
}

        这里有可能出现竞争条件,其原因是对count的访问未作限制。有可能出现以下情况:缓冲区为空,消费者刚刚读取count的值发现它为0,此时调度程序决定暂停消费者并启动运行生产者(进程切换)。生产者向缓冲区加入一个数据项,count加1。现在count的值变成了1,它推断认为count刚才为0,所以消费者此时一定在睡眠,于是生产者调用wakeup来唤醒消费者。

        但是消费者在逻辑上并未睡眠,所以wakeup信号丢失,当消费者下次运行时,它将测试先前读取的count值,发现它为0。于是睡眠,生产者迟早会填满整个缓冲区,然后睡眠,这样一来,两个进程将永远睡眠下去。

信号量的引入及其操作

        信号量是Dijkstra在1965年提出的一种方法,它使用一个整型变量来累计唤醒次数,供以后使用。在他的建议中引入了一个新的变量类型,称作信号量(semaphore)。一个信号量的取值可以为0(表示没有保存下来的唤醒操作)或者正值(表示有一个或多个唤醒操作)。

        Dijkstra建议设立两种操作:down和up(分别为一般化后的sleep和wakeup)。对一个信号量执行down操作,则是检查其值是否大于0。若该值大于0,则将其减1(即用掉一个保存的唤醒信号)并继续;若该值为0,则进程将睡眠,而且此时down操作并未结束检查数值、修改变量值以及可能发生的睡眠操作均作为一个单一的、不可分割的原子操作完成。保证一旦一个信号量操作开始,则在该操作完成或阻塞之前,其他进程均不允许访问该信号量。这种原子性对于解决同步问题和避免竞争条件是绝对必要的。所谓原子操作,是指一组相关联的操作要么都不间断地执行,要么不执行

        up操作对信号量的值增1。如果一个或多个进程在该信号量上睡眠,无法完成一个先前的down操作,则由系统选择其中的一个(如随机挑选)并允许该进程完成它的down操作。于是,对一个有进程在其上睡眠的信号量执行一次up操作后,该信号量的值仍旧是0,但在其上睡眠的进程却少了一个。信号量的值增加1和唤醒一个进程同样也是不可分割的,不会有某个进程因执行up而阻塞,正如前面的模型中不会有进程因执行wakeup而阻塞一样。

        在Dijkstra原来的论文中,他分别使用名称P和V而不是down和up,荷兰语中,Proberen的意思是尝试,Verhogen的含义是增加或升高。

        从物理上说明信号量的P、V操作的含义。 P(S)表示申请一个资源,S.value>0表示有资源可用,其值为资源的数目;S.value=0表示无资源可用;S.value<0, 则|S.value|表示S等待队列中的进程个数。V(S)表示释放一个资源,信号量的初值应该大于等于0。P操作相当于“等待一个信号”,而V操作相当于“发送一个信号”,在实现同步过程中,V操作相当于发送一个信号说合作者已经完成了某项任务,在实现互斥过程中,V操作相当于发送一个信号说临界资源可用了。实际上,在实现互斥时,P、V操作相当于申请资源和释放资源

        该解决方案使用了三个信号量:一个称为full,用来记录充满缓冲槽数目,一个称为empty,记录空的缓冲槽总数;一个称为mutex,用来确保生产者和消费者不会同时访问缓冲区。full的初值为0,empty的初值为缓冲区中槽的数目,mutex的初值为1。供两个或多个进程使用的信号量,其初值为1,保证同时只有一个进程可以进入临界区,称作二元信号量。如果每个进程在进入临界区前都执行down操作,并在刚刚退出时执行一个up操作,就能够实现互斥。

        在下面的例子中,我们实际上是通过两种不同的方式来使用信号量,两者之间的区别是很重要的,信号量mutex用于互斥,它用于保证任一时刻只有一个进程读写缓冲区和相关的变量。互斥是避免混乱所必需的操作。

#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;
void producer(void)
{
	int item;
	while(TRUE)
	{
		item = produce_item();
		down(&empty);				//空槽数目减1,相当于P(empty)
		down(&mutex);				//进入临界区,相当于P(mutex)
		insert_item(item);			//将新数据放到缓冲区中
		up(&mutex);				//离开临界区,相当于V(mutex)
		up(&full);				//满槽数目加1,相当于V(full)
	}
}
void consumer(void)
{
	int item;
	while(TRUE)
	{
		down(&full);				//将满槽数目减1,相当于P(full)
		down(&mutex);				//进入临界区,相当于P(mutex)
		item = remove_item();	   		 //从缓冲区中取出数据
		up(&mutex);				//离开临界区,相当于V(mutex)		
		up(&empty);				//将空槽数目加1 ,相当于V(empty)
		consume_item(item);			//处理取出的数据项
	}
}

 

        信号量的另一种用途是用于实现同步,信号量full和empty用来保证某种事件的顺序发生或不发生。在本例中,它们保证当缓冲区满的时候生产者停止运行,以及当缓冲区空的时候消费者停止运行。

        对于无界缓冲区的生产者—消费者问题,两个进程共享一个不限大小的公共缓冲区。由于是无界缓冲区(仓库是无界限制的),即生产者不用关心仓库是否满,只管往里面生产东西,但是消费者还是要关心仓库是否空。所以生产者不会因得不到缓冲区而被阻塞,不需要对空缓冲区进行管理,可以去掉在有界缓冲区中用来管理空缓冲区的信号量及其PV操作。

Semaphore mutex = 1; 
Semaphore full = 0; 
int in = 0,out = 0;
void producer(void)
{
	while(TRUE)
	{
		item = produce_item();
		P(mutex);				//进入临界区
		Buffer(in) = item;			//新生产的数据项放入缓冲区
		in = in + 1;				//因无界,无需考虑输入指针越界
		V(mutex);				//离开临界区
		V(full);				//增加已用缓冲区的数目
	}
}
void consumer(void)
{
	int item;
	while(TRUE)
	{
		P(full);			//等待已用缓冲区的数目非0
		P(mutex);			//进入临界区
		item = Buffer(out);		//新生产的数据项放入缓冲区
		out = out + 1;			//因无界,无需考虑输出指针越界
		V(mutex);			//离开临界区
		consume_item(item);		//处理取出的数据项
	}
}

        在计算机领域,同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程会一直等待下去。直到收到返回信息才继续执行下去。异步是指进程不需要一直等待下去,而是继续执行下面的操作,不管其他进程的状态,当有消息返回时,系统会通知进程进行处理,这样可以提高效率。

进程同步与互斥

        在操作系统中,进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)。但对于某些资源来说,其在同一时间只能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源。典型的临界资源比如物理上的打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。

        对临界资源的访问,必须是互斥地进行。也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被成为临界区。

        进程同步也是进程之间直接的制约关系,是为完成某种任务而建立的两个或多个进程,这些进程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系来源于他们之间的合作。比如说进程A需要从缓冲区读取进程B产生的信息,当缓冲区为空时,进程B因为读取不到信息而被阻塞。而当进程A产生信息放入缓冲区时,进程B才会被唤醒

 

       进程互斥是进程之间的间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待。只有当使用临界资源的进程退出临界区后,这个进程才会解除阻塞状态。比如进程B需要访问打印机,但此时进程A占有了打印机,进程B会被阻塞,直到进程A释放了打印机资源,进程B才可以继续执行,概念如下图所示。

 

       进程的同步和互斥是指进程在推进时的相互制约关系。 进程同步源于进程合作,是进程间共同完成一项任务是直接发生相互作用的关系进程互斥源于对临界资源的竞争,是进程之间的间接制约关系。

       实现临界区互斥访问的基本方法有硬件实现方法和信号量方法。

       通过硬件实现临界区最简单的办法就是关CPU的中断。从计算机原理我们知道,CPU进行进程切换是需要通过中断来进行。如果屏蔽了中断那么就可以保证当前进程顺利的将临界区代码执行完,从而实现了互斥。这个办法的步骤就是:屏蔽中断—执行临界区操作—开中断。但这样做并不好,这大大限制了处理器交替执行任务的能力。并且将关中断的权限交给用户代码,那么如果用户代码屏蔽了中断后不再开,那系统岂不是跪了?

       信号量实现方式,这也是我们比较熟悉P/V操作。通过设置一个表示资源个数的信号量S,通过对信号量S的P和V操作来实现进程的的互斥。P/V操作是操作系统的原语,意味着具有原子性。P操作首先减少信号量S,表示有一个进程将占用或等待资源,然后检测S是否小于0,如果小于0则阻塞,如果大于0则占有资源进行执行。V操作是和P操作相反的操作,首先增加信号量S,表示占用或等待资源的进程减少了1,然后检测S是否小于0,如果大于0则唤醒等待使用S资源的其它进程。前面的生产者—消费者问题就是典型的应用信号量解决的进程同步问题。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

信号量与生产者消费者问题 的相关文章

  • Unity Remote5 使用

    Unity Remote是Unity公司提供的一个移动端同步调试工具 xff0c 在Unity编辑器中以播放模式运行项目时 xff0c 该应用程序将与Unity连接 编辑器的可视输出被发送到设备的屏幕 xff0c 实时输入被发送回Unity
  • 里氏替换原则

    里氏替换原则主要是发生在父类和子类之间 xff0c 说到父类和子类 xff0c 在面向对象的语言中 xff0c 继承是必不可少的 非常优秀的语言机制 xff0c 它有如下优点 xff1a 代码共享 xff0c 减少创建类的代码量 xff0c
  • Android中的Intent

    Android中的Intent可以用来在一个组件中启动App中的另一个组件或者是启动另一个App的组件 xff0c 这里所说的组件指的是Activity Service以及Broadcast 目录 Intent的用途 Intent的类型 I
  • Android中Intent用法详细解释

    Android中一些常见的Intent的习惯用法 xff0c 比如如何通过Intent发送短信 发送邮件 启动摄像机拍照录视频 设置闹铃 打开WIFI设置界面等等 目录 发送短信 发送邮件 打电话 拍照 摄像 发送短信 发送短信的时候 xf
  • Android中的Intent的用法

    文章目录 调用拨号程序发送短信或彩信通过浏览器打开网页发送电子邮件显示地图与路径规划播放多媒体选择图片拍照获取并剪切图片打开手机应用市场安装程序卸载程序进入设置界面 调用拨号程序 span class token comment 调用拨打电
  • oracle vm virtualbox 卸载不了

    只能用很暴力的方法下载了个 Windows Install Clean Up 地址 xff1a https www baidu com s tn 61 02003390 42 hao pg amp wd 61 Windows 20Insta
  • vsftpd服务----配置

    首先安装 Linux 企业版第一张光盘中的vsftpd 2 0 1 5 i386 rpm rpm ivh media cdrom RedHat RPMS vsftpd 3 0 1 5 i386 rpm 启动vsftpd服务 service
  • 排列组合

    排列组合的基本公式为 实现A xff0c C xff0c 并把他们封装称为一个函数 xff0c 之后使用起来就会比较方便 int A int n int m int res 61 1 for int i 61 n i gt n m i re
  • vue3.0常用eslint配置详解 .eslintrc.js

    规则值 34 off 34 或者 0 xff0c 关闭 34 warn 34 或者 1 xff0c 警告 34 error 34 或者 2 xff0c 报错 eslintrc js配置 module span class token pun
  • 实际项目中使用Postsharp

    我现在的项目中使用了winform net2 0 43 asp net mvc net3 5 43 sqlserver2000 xff0c Orm使用的是Castle的ActiveRecord 客户端与服务器端通信使用的是Ice xff0c
  • 开启SELINUX真的就那么难吗?

    我们都知道Linux的安全性要高于windows xff0c 可是你明白Linux到底比windows的安全性高在哪里吗 xff1f 有人在部署环境的时候 xff0c 开局两件事关防火墙 关Selinux xff0c 请问你把Linux的这
  • SSM框架——详细整合教程(Spring+SpringMVC+MyBatis)

    1 1 Spring Spring是一个开源框架 xff0c Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架 xff0c 由 Rod Johnson 在其著作 Expert One On One J2EE Devel
  • 服务器不支持ipv6,怎么解决

    让服务器开发人员修改到上面的测试ipv6通过为止 如果服务器不会修改怎么办 找专业的人员帮忙 有可能需要购买中转服务 本解决方案的花钱找人帮忙部分有介绍 关于ipv6审核给你点借鉴 此文介绍了一些服务器如何适配ipv6 需要添加aaaa中转
  • VS2015远程连接虚拟机

    目录 一 安装VC Linux exe 二 打开VS 选择新建项目 三 配置VS 四 创建项目 一 安装VC Linux exe 下载地址 xff1a 二 打开VS 选择新建项目 三 配置VS 打开VS xff0c 菜单栏点击工具 gt 选
  • 成都富华力鼎:短视频脚本有哪些类型

    制作短视频 xff0c 一个好的脚本是成功的关键 很多小伙伴没有提前写脚本 xff0c 在拍摄的时候就会遇到各种各样的问题 xff0c 场景不适合 员不适合 临时改内容等等 短视频脚本有哪些类型 1 提纲脚本 提纲脚本 xff1a 应用在纪
  • Android开发-Android 10 的新功能及新特性

    前 言 Google 在去年 xff08 2019年 xff09 3月份首次公布了 Android 10 的测试版 xff0c 然后在去年 5 月份的 Google 年度 I O 开发者大会上展示了 Android 10 系统的几个新的功能
  • asp.net动态切换主题

    C 代码 protected void Page PreInit object sender EventArgs e if Request QueryString 34 theme 34 61 null switch Request Que
  • datatable中button

    function test 34 ruleDataTable2 34 dataTable 34 sAjaxSource 34 dbLinkUrl sqlResultexport 34 fnServerData 34 createShowin
  • 重构——使用多态替换switch

    好吧 xff0c 我这个菜鸟确实是常常在用面向过程的思想在考虑问题 xff0c 在编写程序 现在我已经摈弃了自己对java语言个人的偏见 xff0c 而是用平等公平的态度看待java和C 43 43 他们各有千秋 xff0c 各有乾坤的 好

随机推荐

  • HttpUtil

    package com cmb utils import com fasterxml jackson databind ObjectMapper import org apache http Header import org apache
  • springboot异步请求

    场景 xff1a 用户注册的时候会发送短信和邮件 xff0c 注册成功和发送短信 邮件解耦后会提高响应效率 启动类添加注解 64 EnableAsync 64 SpringBootApplication public class Appli
  • Linux安装Terminator

    大家在使用Linux系统的时候 xff0c 有很大一部分时间都是和系统的终端打交道 时间久了会不会有一种厌烦的感觉呢 xff1f xff08 我是一个始终如一的人 xff0c 怎能厌烦呢 xff1f xff09 x1f604 ubuntu下
  • 安装软件或者依赖包时显示错误:unable to locate package zliblg-dev

    在网上查了很久 xff0c 有人说需要更新一下 sudo apt get update 但是还是不行 xff0c 然后我把依赖包中的英文字母l改为阿拉伯数字1就好了 xff0c 哈哈 xff0c 就是这莫简单
  • ranger命令

    ranger命令 ranger主要用来在终端浏览文件的 使用起来也比较优于平时常用的cd命令 安装 span class token function sudo span span class token function apt get
  • firewalld 和 docker 冲突问题

    造成冲突的主要原因是 xff1a iptables的存在 firewalld 和 iptables 首先 xff0c firewalld 和 iptables 都不是防火墙 xff0c 它们只是防火墙的管理程序 xff0c 真正的防火墙是内
  • Android Studio项目中各目录的图标含义

    对初学安卓的人 xff0c 熟悉Android Studio上的各模块都要花力气 打开团队的一个工程 xff0c 对各目录上显示的图标有圆点 方块 三条柱形等等 xff0c 真的一脸茫然 所以本文记录对工程中的文件图标的含义 xff0c 方
  • base64转换

    String data 61 34 9j 4AAQSkZJRgABAQEAZABkAAD 2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8
  • Android中dispatchDraw分析

    Android中dispatchDraw分析 View中 xff1a public void draw Canvas canvas 1 Draw the background 绘制背景 2 If necessary save the can
  • fastboot flash system.img总失败

    7 0之后 system img会很大 xff0c 有时fastboot会很长时间 xff0c 甚至会报错 xff0c 可以用下面的方法 fastboot flash S 256M system system img
  • CAS新版本(6.0-RC4)使用介绍(一)

    新版本CAS介绍 xff08 6 0 RC4 xff09 简介 Central Authentication Service CAS xff0c 通常称为CAS CAS是一种针对Web的企业多语言单点登录解决方案 xff0c 并尝试成为您的
  • 彻底理解Java反射以及动态代理中对反射的应用

    反射 Reflection 是 Java 的特征之一 xff0c 它允许运行中的 Java 程序获取自身的信息 xff0c 并且可以操作类或对象的内部属性 简而言之 xff0c 通过反射 xff0c 我们可以在运行时获得程序或程序集中每一个
  • 读懂消息队列:Kafka与RocketMQ

    3月份学完了极客时间的 消息列队高手课 专栏 xff0c 专栏讲解了许多消息队列的基础知识并且对Kafka与RocketMQ两种主流消息队列有精彩的对比分析 学完专栏后将所有要点整理为笔记记录下来 xff0c 其他相关知识也搜索了大量资料
  • ubuntu20.04设置静态IP地址

    ubuntu20 04 默认使用动态IP设置 xff0c 但有时我们需要为其设置静态IP 本文将带着大家彻底搞清楚ubuntu20 04的IP设置方法 如果你是在虚拟机中使用ubuntu20 04 并对虚拟机的网络设置有疑问的话请看本人的拙
  • Centos的repos文件中的$releasever和$basearch的取值

    查看CentOS Base repo部分内容 xff0c 文件路径 etc yum repos d CentOS Base repo base baseurl 61 http mirror centos org centos release
  • 如何用Redis实现分布式锁

    为什么需要分布式锁 在聊分布式锁之前 xff0c 有必要先解释一下 xff0c 为什么需要分布式锁 与分布式锁相对就的是单机锁 xff0c 我们在写多线程程序时 xff0c 避免同时操作一个共享变量产生数据问题 xff0c 通常会使用一把锁
  • AQS与Synchronized异曲同工的加锁流程

    在并发多线程的情况下 xff0c 为了保证数据安全性 xff0c 一般我们会对数据进行加锁 xff0c 通常使用Synchronized或者ReentrantLock同步锁 Synchronized是基于JVM实现 xff0c 而Reent
  • 基于贫血模型的MVC开发模式VS领域建模开发模式

    大部分工程师都是做业务开发的 xff0c 很多业务系统都是基于MVC三层架构来开发的 xff0c 确切点说 xff0c 这是一种基于贫血模型的MVC三层架构开发模式 这种开发模式已经成为标准的Web项目开发模式 xff0c 但它却违反了面向
  • 从Servlet、Servlet容器及Web容器的演进历程总结框架设计的套路

    Web架构演进历程 早期的Web技术主要用于浏览静态页面 xff0c 首先浏览器发起HTTP请求 xff0c 请求一些静态资源 xff0c 这时候需要一个服务器来处理HTTP请求 xff0c 并且将相应的静态资源返回 这个服务器叫HTTP服
  • 信号量与生产者消费者问题

    生产者 消费者问题 生产者 消费者题型在各类考试 xff08 考研 程序员证书 程序员面试笔试 期末考试 xff09 很常见 xff0c 原因之一是生产者 消费者题型在实际的并发程序 xff08 多进程 多线程 xff09 设计中很常见 x