信号量
POSIX信号量
信号量概念
只要我们对资源进行整体加锁就默认了我们对这个资源整体使用,实际情况可能存在一份公共资源,但是允许同时访问不同的区域!(程序员编码保证不同的线程可以并发访问公共资源的不同区域!)
信号量本质是一把计数器,衡量临界资源中资源数量多少的计数器
只要拥有信号量,就在未来一定能够拥有临界资源的一部分,申请信号量的本质:对临界资源中特定小块资源的预定机制。比如电影院买票预定座位
只要申请成功,就一定有你的资源,只要申请失败,就说明条件不就绪,你只能等,就不需要判断了
线程要进行访问临界资源中的某一区域——得先申请信号量——前提是所有人必须先看到信号量——所以信号量本身必须是:公共资源。
信号量PV操作
- P操作:我们将申请信号量称为P操作(原子的),申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减一,因此P操作的本质就是让计数器减一。
- V操作:我们将释放信号量称为V操作(原子的),释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加++,因此V操作的本质就是让计数器加++。
信号量的核心操作就是PV原语
PV操作必须是原子的:
多个执行流为了访问临界资源会竞争式的申请信号量,因此信号量是会被多个执行流同时访问的,也就是说信号量本质也是临界资源。
但信号量本质就是用于保护临界资源的,我们不可能再用信号量去保护信号量,所以信号量的PV操作必须是原子操作。
注意: 内存当中变量的++、–操作并不是原子操作,因此信号量不可能只是简单的对一个全局变量进行++、–操作。
信号量函数:
sem_init 初始化信号量:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数说明:
sem:需要初始化的信号量。
pshared:传入0值表示线程间共享,传入非零值表示进程间共享。
value:信号量的初始值(计数器的初始值)。
返回值说明:
初始化信号量成功返回0,失败返回-1。
注意: POSIX信号量和System V信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的,但POSIX信号量可以用于线程间同步。
sem_destroy 销毁信号量:
int sem_destroy(sem_t *sem);
参数说明:
sem:需要销毁的信号量。
返回值说明:
销毁信号量成功返回0,失败返回-1。
sem_wait 等待信号量(申请信号量):
int sem_wait(sem_t *sem);
参数说明:
sem:需要等待的信号量。
返回值说明:
等待信号量成功返回0,信号量的值减一。
等待信号量失败返回-1,信号量的值保持不变。
sem_post 发布信号量(释放信号量):
int sem_post(sem_t *sem);
参数说明:
sem:需要发布的信号量。
返回值说明:
发布信号量成功返回0,信号量的值加++。
发布信号量失败返回-1,信号量的值保持不变。
基于环形队列的生产消费模型
上一篇博文的生产者-消费者的例子是基于queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序。环形队列采用数组模拟(推荐数组模拟,因为缓存命中率高),用模运算来模拟环状特性。此环形队列的生产者消费者模型中,生产线程用来放数据,消费线程用来拿数据。
引入环形队列
环形队列之前我们就了解过了,只要是环形队列,就存在判空判满的问题。实际上并不是真正的环形队列,而是通过数组模拟的,当数据加入到最后的位置时直接模等于数组的大小即可。通常情况下,判空判满的问题我们是通过空出一个位置,当两个指针指向同一个位置的时候是空,当只剩一个位置的时候就是满,但是我们这里不需要关注。
访问环形队列
生产者和消费者访问同一个位置的情况:空的时候,满的时候;其他情况下生产者与消费者访问的就是不同的区域了。
为了完成环形队列的生产消费,我们的核心工作就是
-
1.消费者不能超过生产者
-
2.生产者不能套消费者一个圈以上
-
3.生产者和消费者指向同一个位置时,如果此时满了就让消费者先走,如果此时为空就让生产者先走
我们现在不需要考虑环形队列为满为空的,因为有信号量帮我们考虑。当我们不考虑留一个空位置的时候,发生两个执行流访问同一个位置只有当环形队列为满或为空的情况(互斥 & 同步)。其它的时候,我们都指向不同的位置。
当空的时候,消费者不能消费,而要让生产者生产,当满的时候,生产者不能生产,而要让消费者消费,这是互斥的体现
我们不能让二者同时访问一个位置并执行,要具有一定的顺序写,这是同步的体现
当二者指向不同的位置时,二者可以同时运行,这是并发的体现。
如果生产者和消费者两个执行流指向了同一个位置,那么它们谁先运行呢?解决此问题,需要分类讨论(为空 & 为满),答案如下:
- 空:消费者不能超过生产者,因为消费者不能读取到还没生产的数据 ——> 生产者先运行
- 满:生产者不能把消费者套一个圈继续再往后写入,因为这就会把你曾经生产还未被消费者拿到的数据给覆盖了 ——> 消费者先运行
注意:上述的原则是由信号量来保证的。信号量是用来描述临界资源数目多少的计数器。我们通过信号量保证了生产者和消费者的串行化过程。而除了这两种情况之外,生产者和消费者指向的都不是同一个位置,因此该环形队列当中不可能会出现数据不一致的问题。并且大部分情况下生产者和消费者指向并不是同一个位置,因此大部分情况下该环形队列可以让生产者和消费者并发的执行
大部分情况下生产者与消费者是并发执行的,但是当环形队列为空或为满的时候就会存在着同步与互斥问题。
如何去进行保证:信号量维护,信号量是衡量临界资源中资源数量的
资源是什么:
1.对于生产者,看中的是队列中的剩余空间,空间资源定义成一个信号量
2.对于消费者,看中的是队列中的数据资源,数据资源定义成一个信号量
比如我们一共有10个位置,消费者初始信号量是0,生产者初始信号量是10,如果生产者线程生产数据,申请信号量,进行P操作,信号量变为9,申请失败则阻塞;申请成功后消费者线程看到了多一个数据资源,消费者信号量进行V操作.所以我们并不需要进行判空判满:当生产者生产满了,信号量申请不到,进行阻塞,只能让消费者先走;当消费者消费完了,信号量申请不到,只能让生产者先走
生产者和消费者的申请和释放资源
对于生产者和消费者来说,它们关注的资源是不同的:(我们假设环形队列的空间是N)
-
生产者关注的是环形队列当中是否有空间(room),只要有空间生产者就可以进行生产。其空间变化应该是 N -> 0
-
消费者关注的是环形队列当中是否有数据(data),只要有数据消费者就可以进行消费。其数据变化是0 -> N
现在我们用信号量来描述环形队列当中的空间资源(roomSem)和数据资源(dataSem),在我们初始信号量时给它们设置的初始值是不同的:
-
roomSem的初始值我们应该设置为环形队列的容量,因为刚开始时唤醒队列中全是空间
-
dataSem的初始值我们应该设置为0,因为刚开始时环形队列中没有数据
生产者申请空间资源,释放数据资源:
对于生产者而言,每次生产数据前要先申请空间资源rootSem
- 如果roomSem的值不为0,数据不满,还有空位放数据,则信号量申请成功,此时生产者可以进行生产操作,放入数据
- 如果roomSem的值为0,数据满了,没有空位放数据,则信号量申请失败,此时生产者需要在roomSem的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒(当消费者消费数据后)。
当生产者生产完数据后,应该释放数据资源dataSem - 虽然生产者在进行生产前是对rootSem进行的P操作,但是当生产者生产完数据,应该对dat