信号量
信号量 : 表示系统中某种资源的数量, 当它的值大于0时, 表示当前可用资源的数量; 当它的值小于0时, 其绝对值表示等待使用该资源的进程个数
P, V操作 : PV操作由P操作原语和V操作原语(不可中断)组成,针对信号量进行相应的操作. P操作相当于请求资源, V操作相当于释放资源
信号量的分类
整型信号量
本质就是一个数, 表示资源数量
int S = 1; // 整型信号量, 初始值为1, 表示系统中有一个资源
void P(int S){ // P操作
while(S <= 0); // 资源数不够则循环等待
S = S - 1; // 分配资源, 资源数-1
}
void V(int S){ // V操作
S = S + 1;
}
整型信号量的问题 : 存在"忙等", 即上述P操作时, 如果资源不够, 将一直执行while循环语句, 该进程会一直占用CPU, 为解决这个问题, 引入了记录型信号量
记录型信号量
除了记录资源数, 还加入了等待队列
定义如下:
typedef struct{
int value; // 剩余资源数
struct process *L; // 等待队列
}semaphore;
? 对应的P, V操作实现如下:
void P(semaphore S){
S.value--;
if(S.value < 0){
block(S.L); // 使进程从运行态 -> 阻塞态
} // 如果剩余资源不够, 利用block原语使进程将进程挂起到S的等待队列(阻塞队列)中, 避免"忙等"
}
void V(semaphore S){
S.value++;
if(S.value<=0){
wakeup(S.L); // 使进程从阻塞态 -> 就绪态
} // 释放资源后, 等待队列还有进程, 那么利用wakeup原语唤醒该进程
}
后文所使用的信号量均为semaphore即记录型信号量, 一般我们所说的信号量也均为记录型
利用信号量实现同步与互斥
同步
同步 : 保证"一前一后"执行两个操作
利用信号量实现同步 :
semaphore S = 0; // 初始化信号量 = 0
P1(){ // P1进程
xx1; // 操作1
xx2; // 操作2
V(S); // 信号量++
}
P2(){
P(S);
xx3; // 操作3
xx4; // 操作4
}
总结就是 : 在"前"操作之后执行V操作, 在"后"操作之前执行P操作
互斥
互斥 : 实现对临界资源(一次只能供一个进程访问的资源)的访问
利用信号量实现互斥:
semaphore mutex = 1; // 互斥信号量mutex, 初始化为1
P1(){
P(mutex);
访问临界区;
V(mutex);
}
P2(){
P(mutex);
访问临界区;
V(mutex);
}
生产者-消费者问题
问题本质 : 实现对一个大小为n的缓冲区的互斥访问, 存取操作
问题描述:
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
关键点 :
- 生产者消费者共享一个大小为n, 初始为空的缓冲区----缓冲区即临界资源
- 缓冲区未满时生产者才可以将产品放入----设置empty信号量
- 缓冲区不为空时消费者才可以将产品取出----设置full信号量
实现 :
信号量设置 :
semaphore mutex = 1; // 互斥信号量, 实现对缓冲区的互斥访问
semaphore empty = n; // 同步信号量, 表示空闲缓冲区数(可放产品数)
semaphore full = 0; // 同步信号量, 表示非空缓冲区数(放入产品数)
生产者消费者操作:
producer(){ // 生产者
while(1){
P(empty);
P(mutex);
产品放入缓冲区;
V(mutex);
V(full);
}
}
consumer(){ // 消费者
while(1){
P(full);
P(mutex);
取出产品;
V(mutex);
V(empty);
}
}
tips :
- P(mutex)互斥操作必须在同步操作之后, 否则会引发"死锁":
// 如果改变 P(mutex)和P(empty)顺序, 假设此时empty=0,即缓冲区已满
producer(){ // 生产者
while(1){
P(mutex);
P(empty);
产品放入缓冲区;
V(full);
V(mutex);
}
}
比如我生产者先P(mutex)申请到了临界资源访问权限, 但是之后P(empty)时被阻塞, 此时消费者一方又由于mutex被生产者占有而无法取出产品, 导致互相等待对方释放资源, 即死锁
- 在此处, 如果缓冲区大小为1, 可以不设置mutex信号量(互斥可以由empty和full满足)
吸烟者问题
问题本质 : 可以生产多个产品的单生产者问题
问题描述 :
假设一个系统中有三个抽烟者进程,每个抽烟者不断地卷烟并抽烟。抽烟者卷起并抽掉一颗烟需要有三种材料:烟草、纸和胶水。一个抽烟者有烟草,一个有纸,另一个有胶水。系统中还有两个供应者进程,它们无限地供应所有三种材料,但每次仅轮流提供三种材料中的两种。得到缺失的两种材料的抽烟者在卷起并抽掉一颗烟后会发信号通知供应者,让它继续提供另外的两种材料。这一过程重复进行。
关键点 :
-
临界资源---桌子, 视为缓冲区, 大小为1 ---- 设置同步信号量finish
-
产品有3种不同的组合, 分别给3个不同的人使用 ---- 设置同步信号量offer1, offer2, offer3
-
生产者如何实现轮流生产3种产品 ---- for i in range(3)即可
注意这里不需要设置额外的互斥信号量mutex, 因为缓冲区大小为1
实现 :
信号量设置:
semaphore offer1 = 0, offer2 = 0, offer3 = 0;
semaphore finish = 0; // 抽烟是否完成
int i = 0; // 实现"轮流生产"
provider(){
while(1){
if(i == 0)
V(offer1);
else if(i == 1)
V(offer2);
else if(i == 2)
V(offer3);
i = (i + 1) % 3;
P(finish); // 注意由于finish初值为0,所以将P(finish)放在后面
}
}
smoker1(){
while(1){
P(offer1); // 拿烟,抽了
V(finish); // 完成抽烟,告诉生产者可以继续生产下一个了
}
}
smoker2(){
while(1){
P(offer2); // 拿烟,抽了
V(finish); // 完成抽烟,告诉生产者可以继续生产下一个了
}
}
smoker3(){
while(1){
P(offer3); // 拿烟,抽了
V(finish); // 完成抽烟,告诉生产者可以继续生产下一个了
}
}
读-写者问题
问题本质 : 允许多个进程同时读缓冲区, 但是只允许一个进程写缓冲区
问题描述 :
一个共享文件, 可以有多个读者同时读文件, 或者一个写着向文件中写信息, 任一写者完成写操作前不允许其他读 / 写者工作, 写者执行写操作前所有的读者应当退出
关键点 :
- 实现多个读者同时读
- 实现读者-写者,写者-写者之间的互斥
实现 :
方案一
信号量设置 :
semaphore rw = 1; // 实现对文件的互斥访问
int count = 0; // 记录读者的数目
semaphore mutex = 1; // 实现互斥
写者 :
writer(){
while(1){
P(rw);
write......... // 写文件
V(rw);
}
}
读者 :
reader(){
while(1){
P(mutex); // 这里的mutex进用于实现count的互斥, 防止两个读者同时进入时出问题
if(count == 0)
P(rw); // 第一个读者进来时将文件"锁定"
count++; // 每来一个读者,count+1
V(mutex);
read........ // 读文件
P(mutex)
count--;
if(count == 0)
V(rw); // 最后一个读者退出时将文件权限释放
V(mutex);
}
}
方案一的问题
仔细分析后, 我们从上述方案中可以发现一个问题, 那就是如果读者源源不断的到来, 写者将一直被挂起"饿死", 这个方案实际上是不公平的, 具有"读进程优先的特性", 为了解决这个问题, 我们可以引入一个新的信号量w=1, 实现读者写者的公平性.
方案二
信号量设置 :
semaphore rw = 1;
int count = 0;
semaphore mutex = 1;
semaphore w = 1; // 方案一基础上增加
写者 :
writer(){
while(1){
P(w);
P(rw);
write......... // 写文件
V(rw);
V(w);
}
}
读者 :
reader(){
while(1){
P(w);
P(mutex);
if(count == 0)
P(rw);
count++;
V(mutex);
V(w); //注意V(w)放在read之前
read........
P(mutex)
count--;
if(count == 0)
V(rw);
V(mutex);
}
}
分析 : 可以看到, 在方案二中, 如果读者读的过程中有写着想要访问, 那么该写者进程将挂在信号量w的等待队列上, 当该读者读完退出后, 写者即可以写, 当一个读者在读的时候, 它已经V(w)操作了, 也不会影响读者读的并行
哲学家就餐问题
问题本质 : 进程需持有多个临界资源才可以工作, 如何避免分配不当导致"死锁"
问题描述 :
哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。
由于有5只筷子, 相当于5个临界资源, 我们定义信号量数组chopstick[5]来表示这5个临界资源
semaphore chopstick[5] = {1,1,1,1,1}
同时为了方便描述, 我们给哲学家和筷子都编号
根据编号有如下定义:
哲学家i 号的左手筷子为chopstick[i] , 右手为chopstick[(i+1)%5]
我们很容易想到一种方式分配临界资源:
方案一
Pi(){ // Pi表示第i个哲学家进程
while(1){
P(chopstick[i]); // 拿左边筷子
P(chopstick[(i + 1) % 5]); // 拿右边筷子
eat...... // 吃饭
V(chopstick[i]); // 放下左边筷子
V(chopstick[(i + 1) % 5]); // 放下右边筷子
}
}
这个方案有一个很明显的问题, 那就是如果五个哲学家同时拿筷子, 那么每个人都将拿起自己左手的筷子而等待右手的筷子 , 也就导致了"死锁"的局面, 如下图:
我们可以通过几种方式改变这种死锁局面, 核心思想均是防止所有人同时拿到1根筷子 :
方案二
描述 : 限制最多四人同时就餐
semaphore cnt = 4; // 限制4个人
semaphore chopstick[5] = {1,1,1,1,1};
semaphore
Pi(){ // Pi表示第i个哲学家进程
while(1){
P(cnt);
P(chopstick[i]); // 拿左边筷子
P(chopstick[(i + 1) % 5]); // 拿右边筷子
eat...... // 吃饭
V(chopstick[i]); // 放下左边筷子
V(chopstick[(i + 1) % 5]); // 放下右边筷子
V(cnt);
}
}
方案三
描述 : 奇数号先拿左手的筷子, 偶数号先拿右手的筷子
semaphore chopstick[5] = {1,1,1,1,1};
Pi(){ // Pi表示第i个哲学家进程
while(1){
if(i % 2 == 1){ // 奇数号
P(chopstick[i]); // 拿左边筷子
P(chopstick[(i + 1) % 5]); // 拿右边筷子
}
else{ // 偶数号
P(chopstick[(i + 1) % 5]); // 拿右边筷子
P(chopstick[i]); // 拿左边筷子
}
eat...... // 吃饭
V(chopstick[i]); // 放下左边筷子
V(chopstick[(i + 1) % 5]); // 放下右边筷子
}
}
方案四
描述 : 互斥"拿筷子"这个动作
semaphore chopstick[5] = {1,1,1,1,1};
semaphore mutex = 1;
Pi(){ // Pi表示第i个哲学家进程
while(1){
P(mutex); // 互斥
P(chopstick[i]); // 拿左边筷子
P(chopstick[(i + 1) % 5]); // 拿右边筷子
V(mutex);
eat...... // 吃饭
V(chopstick[i]); // 放下左边筷子
V(chopstick[(i + 1) % 5]); // 放下右边筷子
}
}
四种方法都很好理解, 总而言之就是不让五个哲学家都陷入等待的局面就ok啦~