下面我们来介绍
如何利用信号量
来解决同步互斥问题
这就是我们这里说的
信号量的使用
那首先呢我们把信号量
可以分成两类
一类叫二进制信号量
它的初值呢是0或者是1
而另一类呢叫资源信号量
它的资源数目呢
可以是任意的非负整数
这两类信号量呢它们俩是等价的
也就说我用其中的一个
可以构造出另一个
可以由第一个构造出第二个
也可以由第二个构造出第一个
在使用信号量来解决
我们的同步互斥问题的时候
我们可以把它分成这样两种情况
一种呢是互斥访问
也就说在用它来控制临界区的互斥
另一个呢是用它来做同步
在两个线程之间实现事件的等待
首先呢我们来看
它具体是如何在做
我们在这里的例子呢
是用信号量实现临界区的互斥访问
这时候做法呢
是用一个信号量
来对应一个临界区
那这时候呢 由于我们说
任何一个临界区在任期一个时刻
只允许一个线程执行临界区的代码
那这时候它的初值是1
好 那我们先是完成它的初始化
在这里呢初值取1
然后我们来看
它如何来控制临界区的访问
那这是它的控制代码
在前面呢有一个P操作
在后面呢有一个V操作
P操作完成申请 V操作完成释放
它怎么来实现这个临界区的互斥呢
首先这个信号量初始值为1
所以第一个线程进来的时候
它由它 把它1变成0
所以它能进去 好
如果说这时候有第二个线程
也想进入临界区
那么刚才第一个还在里头运行
第二个来说它0变成-1
好 那这时候呢
它就在P操作这个地方进入等待状态
好 那这时候
在临界区的第一个线程
它执行结束 进行V操作
那么V操作呢就会导致
它把刚才的这个信号量的计数
由-1改成0
那这时候呢它知道有
第二个线程在里等着呢
好 那么这个时候呢
它唤醒第二个线程
它继续往后走
那这个时候呢 第二个线程
就会在第一个结束的时候进入临界区
那么利用这种做法呢
我们就实现了临界区的互斥访问
那用它来做的时候呢
有几点需要注意的
第一个呢 这里的PV操作
必须成对使用
也就说你有一个申请
有一个释放
如果说 不申请你直接释放
会出现什么情况
就会有多个线程
进入到临界区里头
如果说你只申请不释放
那么这时候呢 就会出现
缓冲区没有任何一个线程
但是谁也进不去的情况
用P操作保证互斥
用V操作保证资源的释放
然后这两个的顺序呢
颠倒或者重复
都会导致可能的麻烦
第二个呢 我们用信号量
来实现条件同步
那这时候呢也是用一个信号量
来描述一个条件同步
那它的初值是0
也就说在起头的时候
那个事件还没出现
出现之后 那这对于我们来说
是一个资源 它就变成1了
好 等待资源你申请
那这时候事件出现之后
那等待的就可以继续往下走
具体怎么做呢
初值它设成为0
我有两个线程A和B
分别有mn和xy各自的两个模块
那我们需要做的事情呢
是在B执行到x之后
这个A才能执行n模块
比如说我在这里这边要准备数据
这边是使用数据
如果说你的数据没准备好
那你要想使用数据
比如说n模块是
对数据进行处理的话
那你就没有可以处理的数据
它们俩之间呢
必须等到B执行完x
A才能执行n
好 那这时候我们怎么办呢
在这里呢 一个P操作一个V操作
在B里头 释放信号量的
好 那么这时候它就会由0变成1
好 如果说进程B
先执行完x 到达这个点
那么这个时候呢
A执行完m的时候
这个地方信号量的值就是1了
好 它就直接能往下执行了
假定不是这种情况
是A先执行完m
那么这时候呢它来申请
进行P操作
这时候呢信号量的初值是0
那么这时候它变成-1
这时候它堵塞 进入等待状态
好 然后这个进程B呢
执行完x到这儿它释放
释放的时候我们刚刚还记得
信号量的V操作的实现里头
它是加一 如果说加完之后
它是小于等于0的
那这时候知道
有另外一个线程A在等待
好 那这时候唤醒A 它继续往后走
这时候线程A呢
也可以继续往下走了
好 所以从这个地方呢
我们就实现了一个条件等待
线程A要等待线程B执行完x模块
它A才能执行n模块
这是我们用信号量
来实现同步的两个基本的情况
和我们这里的基本做法
好 那我们下面呢
来看一个更实际的例子
就是这里我们所说的
生产者 消费者问题
这个问题的基本要求呢是这样的
有一组生产者 一个或者是多个
它们负责产生数据
并且把这数据呢放在缓冲区里头
然后另外呢还有一个消费者
这个消费者呢
负责从缓冲区里读数据
进行后续的处理
比如说我们在这里呢
消费者就是对应我们打印进程
前面呢就是对应着你的应用程序
若干个应用程序
会产生打印作业
放在缓冲区里头
打印服务器呢
负责从缓冲区里
读出你的打印数据
然后进行打印
那这个缓冲区呢有一个限制
就是在任何时刻
只能有一个生产者或者是消费者
可以访问这个缓冲区
不可以两个同时来做
那它们在用的过程当中
可能会有这种情况
生产者往缓冲区里写
那我的缓冲区得有空地才能写
如果写满了 那它做不下去了
消费者从里读数据
里头得有数据它才能读
如果没有数据它得等着
好 我们就来看
如何用信号量实现
生产者 消费者之间的协调关系
我们如何用信号量来解决它呢
我们首先对这个问题呢做一些分析
从我们刚才的描述里呢
大家可以知道
实际上这个缓冲区是一个临界区
任何一个时刻
只允许一个线程对缓冲区进行操作
这可以描述成临界区
除此之外呢还有另外两条限制
第一条呢是说
消费者要从缓冲区里读数据
如果说缓冲区里没有数据 是空的
我消费者是没办法进行下去的
它必须等待生产者
往缓冲区里放了数据之后
它才能够继续往后进行操作
另一个呢是生产者
生产者必须等待缓冲区里头有空地
你才能够往里放
如果缓冲区满了 那生产者必须等待
我们把生产者消费者问题的描述呢
转换成了我们用临界区
和条件等待的方式
给它描述出来
那这三条呢 对应过来就是
我们这里的互斥访问 缓冲区
然后生产者和消费者
分别有一个条件等待
是等待对方的
那这时候呢 我们把它描述出来
就变成这样几个信号量所做的约束
第一个呢是二进制信号量
它描述这里的互斥关系
第二个就是fullBeffers
它对应着 有数据我才能够往外读
所以消费者必须等待缓冲区里有数据
然后再有一个呢 是生产者
生产者必须等待缓冲区有空地
你才能往里写
好 这两者对应这两个信号量
它加起来之后呢
应该是你缓冲区的总的大小
好 有了这些分析之后呢
我们基本上就把我们这里的
生产者消费者这一个同步的问题
转换成我们刚才说的
互斥访问和条件等待
那接下来呢
我们把它转换成实际的代码
首先呢 是对它的初始化
二进制信号量
它的初值是一个临界区 初值为1
好 然后刚开始的时候
缓冲区里没有任何数据
所以缓冲区满
这个地方它的初值是0
好 然后说有多少个空缓冲区呢
每个空缓冲区对应着一个资源
生产者 消费者它是如何在做
这是生产者 这是消费者
它们对应的代码
那在这里呢 往缓冲区里放数据
从缓冲区里读数据
是它们要进行的主要操作
那首先第一个
我们要实现这个是互斥访问临界区
所以在这儿呢
有一个P操作 有一个V操作
是申请这个二进制信号量mutex
好 在这边呢也是一样
好 实现他们俩之间
对缓冲区的互斥访问
它仅有互斥是不够的
如果说缓冲区里头没有空地
你即使申请到了缓冲区的互斥操作
你也没办法操作
因为里头没空地了
而这时候你又把缓冲区占掉了
消费者已经没有办法帮你腾出空地来
所以在这儿之前呢
我需要来检查是否有空缓冲区
有 我可以再进一步检查
是否有另外一个生产者或者消费者
在访问缓冲区
好 如果没有
那它就可以往里放数据了
那这一条有没有缓冲区
它在什么时候会去释放呢
那消费者在从里头读出数据之后
它在这里呢会释放一个空缓冲区资源
也就相当于我给你腾出一个空地来
而另一头呢
消费者要想进入的时候呢
它得看里头是否有数据
好 在这个地方呢 负
那实际上就是我看里头是否有数据
那只有生产者往里写了数据之后
我这个地方呢才能有数据资源
好 那你才可以从里头读数据
生产者在往里写了数据之后
它就释放一个fullBuffer
这样一个资源
释放这个资源之后
实际相当于我这个里头就多了数据
好 这时候这个消费者呢
他们俩是配合
这时候有一个问题
我在这个地方这个顺序可以调整吗
在这里头 如果说
你把这两个顺序调整了
它就会出现死锁
原因是在于我检查空和满
你需要先检查
然后我再去申请互斥访问
如果说你申请了互斥访问
那这个时候你已经占用了临界资源
那别人不能读写了
而这时候你又发现里头是满
或者发现里头是空
那这时候你进行不下去了
但这时候呢别人也没有办法
来申请到这个临界区
那这时候它就会形成死锁
这是用信号量机制
来解决生产者消费者问题
从这个应用的例子当中我们可以看到
实际上用信号量来解决同步问题
它是需要你有技巧的
程序员在写的时候
你必须很好的了解你的问题
和很好的运用信号量机制
并且这时候呢
它是容易出错的
我可能在某个地方
忘了一个P操作
或者忘了一个V操作
或者说我把它的顺序搞倒了
这些呢都是容易出问题的
所以它没有办法避免死锁的出现
你必须在你写程序的时候
来解决这个问题