文章目录
进程间的关系(2⭐)
进程并发与时间有关的错误
// P1
R1 = count;
R1 = R1 + 1;
count = R1;
// P2
R2 = count;
R2 = R2 + 1;
count = R2;
假设开始时count=1
-
如果P1、P2顺序执行,那么最终count将被赋值为3
-
假如系统P1、P2如下顺序执行:
- R1 = count //R1值为1
- R2 = count //R2值为1
- R1 = R1 + 1 //R1值为2
- R2 = R2 + 1 //R2值为2
- count = R1 //count被赋值为R1=2
- count = R2 //count被赋值为R2=2
即最终结果count=2,与P1、P2顺序执行的结果不一致,即错误。
原因
- 共享了变量
count
- “同时”使用了这个变量
count
解决方法
- 取消共享(不现实)
- 允许共享,不允许同时使用**(可行)即进程互斥**
进程间的两种形式的制约关系
间接相互制约关系(互斥关系)
进程间要通过某种中介发生联系,是无意识、无安排的。
用户要使用这类资源之前需要提出申请,不能直接使用。
直接相互制约关系(同步关系)
进程间的相互联系是有意识的、有安排的。
多个进程中发生的事件存在某种时序关系,相互合作完成一项任务。
一个进程运行到某一点时要求另一伙伴进程为它提供消息,在未获得消息之前,该进程处于等待/阻塞状态,获得消息后被唤醒进入就绪态。
临界资源
系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源/互斥资源,也可以是软件的共享变量。
临界区
在进程中涉及到临界资源的程序段。
信号量+P/V操作控制进程(5⭐)
信号量初始值可用资源的数目
信号量仅通过P或V操作来访问
P操作:wait(s)或P(s)
V操作:signal(s)或V(s)
PV操作具有原子性
原子性:指事务的不可分割性,一个事务的所有操作要么不间断地全部被执行,要么一个也没有执行。
整形信号量
将信号量S定义成一个整数。
获取资源P操作
以下semaphore与int类型相同
void P(semapore *s) // 或wait
{
while(s <= 0); // 无操作 do no-op
s--;
}
释放资源V操作
void V(semapore *s) // 或signal
{
s++;
}
利用信号量实现进程互斥访问临界资源
semapore s = 1;
Process
{
P(&s);
操作C;
V(&s);
}
记录信号量
除了代表资源数目的value外,增加进程链表L,用于链接所有等待进程。
// 结构
struct semapore
{
int value;
PCB L;
}
// wait
void P(semaphore *s)
{
s.value--;
if(s.value < 0)
{
block(s.L);
}
}
// signal
void V(semaphore *s)
{
s.value++;
if(s.value <= 0)
{
wakeup(s.L);
}
}
semaphore s;
s.value = 1;
Process:
{
P(&s);
操作C;
V(&s);
}
经典的进程同步问题
生产者-消费者问题
这种问题是相互合作进程关系的一种抽象。
- 生产者和消费者必须互斥的使用
- 缓冲区缓冲区空时,消费者不能读取数据
- 缓冲区满时,生产者不能添加数据
利用记录型信号量解决
单缓冲区
生产者进程P和消费者进程C共用一个缓冲区,P生产产品放入缓冲区,C从缓冲区取产品来消费。
单缓冲区出现主要矛盾
- 生产者和消费者不知道缓冲区内是不是有产品,进而一直在“摸索”,占用处理机和临界资源,造成资源利用低。
所以需要一个或多个变量给生产者和消费者“说”是不是空的。
信号量解
- empty信号量:代表缓冲区内空缓冲区的个数,初始状态缓冲区为空,单缓冲区里empty==1表示缓冲区空。
- full信号量:代表缓冲区内满缓冲区的个数,初始状态缓冲区为空,即full==0。
伪代码
semaphore empty = 1, full = 0;
item buffer;
void producer()
{
while(1)
{
produce an itemp;
wait(empty); // P(empty)
buffer = itemp;
signal(full); // V(full)
}
}
void consumer()
{
while(1)
{
wait(full); // P(full)
itemc = buffer;
signal(empty); // V(empty)
consume the itemc;
}
}
void main()
{
producer();consumer(); // 同时运行
}
多缓冲区
多缓冲区的主要矛盾
- 生产者和消费者不知道应该放和拿缓冲区的哪个位置,如果是多个生产者和消费者,很有可能造成位置冲突,即一个位置“存放多个数据”。
伪代码
int in = 0, out = 0;
// 缓冲区
item buffer[n];
// 信号量
semaphore mutex = 1, empty = n, full = 0;
// 生产者
void producer()
{
while(1) // 始终为TRUE
{
produce an item nextp;
wait(empty); // P(empty)
wait(mutex); // P(mutex)
buffer[in] = nextp;
in = (in + 1) % n;
signal(mutex); // V(mutex)
signal(full); // V(full)
}
}
// 消费者
void consumer()
{
while(1)
{
wait(full); // P(full)
wait(mutex); // P(mutex)
nextc = buffer[out];
out = (out + 1) % n;
signal(mutex); // V(mutex)
signal(empty); // V(empty)
consume the item in nextc;
}
}
void main()
{
producer();consumer(); // 同时运行
}
无论是生产者进程还是消费者进程,signal/V操作的次序无关紧要,但**wait/P操作的次序不能随意交换,否则可能造成死锁**。
***e.g.***假如交换P操作
// 生产者
void producer()
{
while(1)
{
produce an item nextp;
wait(mutex);
wait(empty);
buffer[in] = nextp;
in = (in + 1) % n;
signal(mutex);
signal(full);
}
}
// 消费者
void consumer()
{
while(1)
{
wait(mutex);
wait(full);
nextc = buffer[out];
out = (out + 1) % n;
signal(mutex);
signal(empty);
consume the item in nextc;
}
}
按照以下顺序执行:
- 生产者生产产品
- 消费者wait(mutex)
此时mutex=0- 消费者wait(empty)
此时full=-1,消费者被阻塞- 生产者wait(mutex)
此时mutex=-1,生产者被阻塞
此时谁运行都将被阻塞,造成系统死锁!
为何引用mutex?empty和full不够使用?
在多缓冲区示例中,empty和mutex都可以是>1的值,所以会导致多个进程进入临界区,这样违背了进程的互斥访问。
所以仅使用empty和full,并不能实现进程的互斥访问,所以需要添加一个mutex(初始为1,即可以最多允许一个进程访问临界区),进而实现互斥访问临界区(即mutex作为进程进入临界区的“门票”)。
循环队列
- 队空判断:front==rear
- front(生产者-消费者问题为out)指向最先进入的满缓冲区。
- rear(生产者-消费者问题为in)永远指向下一个空缓冲区。
读者-写者问题
利用记录型信号量解决
类似文件读写的逻辑。
信号量设置
w_mutex
互斥量。写者与其他读者/写者互斥地访问。
初值为1。
mutex
互斥量。互斥访问临界资源read_count。
初值为1。
变量
reader_count
读者计数,初值为0。
File file;
int read_count = 0;
semaphore w_mutex = 1, mutex = 1;
// 读者
void reader()
{
while(1)
{
wait(mutex); // 占用读者计数变量使用权
read_count++; // 读者计数+1
if(read_count == 1) // 如果+1后计数为1,则需要抢占写使用权
wait(w_mutex);
signal(mutex); // 释放读者计数变量使用权
read(file); // 读文件
wait(mutex);
read_count--;
if(read_count == 0) // 当前进程-1后如为0,释放写使用权
signal(w_mutex);
signal(mutex); // 释放释放读者计数变量使用权
}
}
// 写者
void writer()
{
while(1)
{
wait(w_mutex);
write(file);
signal(w_mutex);
}
}
读写公平的的读者写者问题
读者优先:只要有一个读者存在,写者无法写,读不断进入,长时间无法写。
读写公平:阻止新读者继续进入
File file;
int read_count = 0;
semaphore w_mutex = 1, mutex = 1, w = 1;
// 读者
void reader()
{
while(1)
{
wait(w); // 如被抢占,则不允许读者继续进入
wait(mutex); // 占用读者计数变量使用权
if(read_count == 0) // 如果是第一个来的写者,则需要抢占修改权
wait(w_mutex);
read_count++; // 读者计数+1
signal(mutex); // 释放读者计数变量使用权
signal(w);
read(file); // 读文件
wait(mutex);
read_count--;
if(read_count == 0) // 如是最后写者,释放文件修改权
signal(w_mutex);
signal(mutex); // 释放释放读者计数变量使用权
}
}
// 写者
void writer()
{
while(1)
{
wait(w);
wait(w_mutex);
write(file);
signal(w_mutex);
signal(w);
}
}
写者优先的读者-写者问题
一个写者打算写,直到最后一个读者写完后,读者才能拿到使用权。
第一个写者要拿读者的控制权。
File file;
int read_count = 0, write_count = 0; // 读者、写者计数
semaphore
read_in = 1, // 控制新读者进入的信号量
read_count_mutex = 1, // 读者计数器控制权
write_count_mutex = 1, // 写者计数器控制权
file = 1; // 文件控制权
// 读者
void reader()
{
while(1)
{
wait(read_in); // 如被抢占,则不允许读者继续进入
wait(read_count_mutex); // 占用读者计数变量使用权
if(read_count == 0) // 如果是第一个来的写者,则需要抢占修改权
wait(file);
read_count++; // 读者计数+1
signal(read_count_mutex); // 释放读者计数变量使用权
signal(read_in);
read(file); // 读文件
wait(read_count_mutex);
read_count--;
if(read_count == 0) // 如是最后写者,释放文件修改权
signal(file);
signal(read_count_mutex); // 释放释放读者计数变量使用权
}
}
// 写者
void writer()
{
while(1)
{
wait(write_count_mutex);
if(write_count == 0)
wait(read_in);
write_count++;
signal(write_count_mutex);
wait(file);
write(file);
signal(file);
wait(write_count_mutex);
write_count--;
if(write_count == 0)
signal(read_in);
signal(write_count_mutex);
}
}
哲学家进餐问题
问题描述
有5个哲学家共用一张圆桌,分别坐在周围的5张椅子上。
在圆桌上有5个碗和**5支(不是5双)**筷子,他们的生活方式是交替地进行思考和进餐。
平时,一个哲学家进行思考,饥饿时便试图取用其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。
进餐完毕后,放下筷子继续思考。
利用记录型信号量解决
因为5支筷子是临界资源,每一支筷子在一段时间内只允许一位哲学家使用。
可以用一个信号量表示一只筷子。
semaphore chopstick[5] = {1, 1, 1, 1, 1};
哲学家活动的描述
semaphore chopstick[5] = {1, 1, 1, 1, 1};
void philosopher(int i)
{
while(1)
{
wait(chopstick[i]); // 拿左筷子
wait(chopstick[(i + 1) % 5]); // 拿右筷子
eat; // 进食
signal(chopstick[i]); // 释放左筷子
signal(chopstick[(i+ 1) % 5]); // 释放右筷子
think; // 思考
}
}
会出现死锁的情况
假如五位哲学家同时饥饿而各自拿起右边的筷子,5个信号量chopstick均为0
当他们再试图拿左边的筷子时,无限期等待。
不是因为对筷子的抢占。
因此,解决哲学家进餐死锁,需要对筷子进行抢占。
上述死锁可采取的解决方案
-
最多允许4个哲学家拿左边的筷子,最终保证最少有一位哲学家可以进餐,用完后释放,从而使更多的哲学家可以用餐。
-
规定奇数号哲学家先拿左边筷子,然后拿右边的筷子,偶数则相反。
例如:
1、2号哲学家竞争1号筷子;
3、4号哲学家竞争3号筷子(竞争奇数号),获得后再去竞争偶数号筷子,
至少能有一位哲学家获得两只筷子进餐。
-
仅当哲学家左右两只筷子都没被使用,才允许哲学家拿起筷子进餐。
-
对筷子进行编号,规定哲学家先取编号小的筷子。
-
如果拿左筷子后发现右筷子拿不到,则放下左筷子,隔一段时间后申请左筷子。
-
对哲学家分为三种状态:思考、饥饿、进食,并且一次拿到两只筷子,否则不拿。
方案1:限制并发数
semaphore chopstick[5] = {1, 1, 1, 1, 1},
count = 4; // 控制并发数目4
void philosopher()
{
while(1)
{
wait(count);
wait(chopstick[i]);
wait(chopsitck[(i + 1) % 5]);
eat;
signal(chopsitck[(i + 1) % 5]);
signal(chopstick[i]);
signal(count);
think;
}
}
一个人吃到饭,其他人均可
方案2:奇偶
semaphore chopstick[5] = {1, 1, 1, 1, 1};
void philosopher(int i)
{
while(1)
{
if(i % 2 == 1) // 奇数
{
wait(chopstick[i]); // 拿左筷子
wait(chopstick[(i + 1) % 5]); // 拿右筷子
}
else //
{
wait(chopstick[(i + 1) % 5]); // 拿右筷子
wait(chopstick[i]); // 拿左筷子
}
eat; // 进餐
signal(chopstick[i]); // 释放左筷子
signal(chopstick[(i+ 1) % 5]); // 释放右筷子
think; // 思考
}
}
多次测试:5号哲学家最容易抢到资源,从而最先吃完释放筷子,解决了死锁。
吸烟者问题
semaphore offer1 = 0, // 吸烟者1需要的物品是否存在
offer2 = 0, // 吸烟者2需要的物品是否存在
offer3 = 0, // 吸烟者3需要的物品是否存在
finish = 0; // 物品是否被消耗掉
// 供应者
void provider()
{
while(1)
{
int i = radom() % 3; // 取随机数
if(i == 0)
{
将草、火放入缓冲区;
signal(offer1);
}
else if(i == 2)
{
将纸、火放入缓冲区;
signal(offer2);
}
else
{
将纸、草放入缓冲区;
signal(offer3);
}
wait(finish);
}
}
// 第一种吸烟者
void smoker1()
{
while(1)
{
wait(offer1);
从缓冲区拿走物品;
signal(finish);
抽烟;
}
}
// 第二种吸烟者
void smoker2()
{
while(1)
{
wait(offer2);
从缓冲区拿走物品;
signal(finish);
抽烟;
}
}
// 第二种吸烟者
void smoker3()
{
while(1)
{
wait(offer3);
从缓冲区拿走物品;
signal(finish);
抽烟;
}
}