基于信号量的进程同步与互斥(1)
基于信号量的进程同步与互斥(1)
本人水平有限,若有错误,欢迎读者及时指出,万分感谢!
1. P/V操作
P/V操作由P操作原语和V操作原语组成,其意义是在一个整型变量S上定义了两个操作,该操作变量被称之为信号量S,只能由P操作和V操作进行修改。S必须置一次且只能置一次初值,S >= 0表示当前可用的资源的数目,S = 1实现互斥(二元信号量mutex),S > 1实现同步(通用信号量)。多个进程要操作同一个临界资源就是互斥,多个进程要按一定的顺序执行就是同步。
- 当一个操作执行P操作原语P(S)时
- S > 0: S := S - 1,进程继续执行;
- S = 0: 进程被阻塞,并将当前进程从运行队列移动到S信号量的队列,直至其他进程在S上执行V操作释放它为止。
- 当一个操作执行V操作原语V(S)时,会顺序执行下述两种操作
- S := S + 1;
- 如果有一个或多个进程在S信号量的队列睡眠(这时S = 1),就会随机唤醒一个进程(将进程从信号量的队列移入就绪队列),并使得其运行后能完成P操作;
- 执行V(S)者继续执行。
2. 使用信号量实现汇合(Rendezvous)
进程A分别要执行a1和a2,进程B分别要执行b1和b2,两个进程在执行过程中一点汇合,直至两者都到后才能继续执行。a1永远在b2之前,b1永远在a2之前,a1和b1的次序不加限制。
定义两个信号量,aArrived
,bArrived
,并且初始化为0,表示a和b是否执行到汇合点。
Thread A:
a1
V(aArrived)
P(bArrived)
a2
Thread B:
b1
V(bArrived)
P(aArrived)
b2
3. 使用信号量实现多路复用(Multiplex)
实现mutex的泛化,使得n个进程能够同时运行在临界区。
设置信号量multiplex = n
。
P(multiplex)
critical section //临界区操作
V(multiplex)
4. 使用信号量实现屏障(Barriers)
对rendezvous进行泛化,使其能够用于多个进程,用于进程组的同步。
// n = the number of threads
count = 0 //到达汇合点的进程个数
semaphore mutex = 1 //保护count
semaphore queue = 0 //进程到达之前都是0,用于进程排队
P(mutex)
count = count + 1
V(mutex)
if count == n : V(queue) //第n个进程到来,唤醒一个进程,触发。
P(queue) //前n-1个进程在此排队
V(queue) //一旦进程被唤醒,有责任唤醒下一个进程
5. “信号量集”机制
5.1 AND型信号量集机制
AND型信号量集是指同时需要多个资源且每种占用一个资源时的信号量集操作,将进程需要的所有共享资源一次全部分配给它,待该进程使用完再一起释放。AND型信号量集P原语为SP,V原语为SV。
SP实现原理
SP(S1, S2, ... , Sn)
if S1 >= 1 and ... and Sn >= 1 then
for i := 1 to n do
Si := Si - 1;
endfor
else
wait in Si; //将进程调度到第一个小于1的信号量Si的等待队列
endif
SV实现原理
SV(S1, S2, ... ,Sn)
for i := 1 to n do
Si := Si + 1;
wake waited process on Si
endfor
5.2 一般信号量集机制
一般信号量集是指同时需要多种资源、每种占用的数目不同、且可分配的资源还存在一个临界值时的信号量处理。
在AND型信号量集的基础上进行补充:进程对信号量集Si的测试值为ti
(用于信号量的判断,即Si >= ti
,表示资源数量低于ti
时,不予分配),资源的申请量为di
(用于信号量的增减,即Si = Si - di
和Si = Si + di
)
SP(S1, t1, d1; ...; Sn, tn, dn)
SP(S1, t1, d1; ...; Sn, tn, dn)
if S1 >= t1 and ... and Sn >= tn then
for i := 1 to n do
Si := Si - di;
endfor
else
wait in Si;
endif
SV(S1, d1; ...; Sn, dn)
SV(S1, d1; ...; Sn, dn)
for i := 1 to n do
Si := Si + di;
wake waited process
endfor
- 特殊情况
- SP(S, d, d):每次申请d个资源,当资源数量少于d个时,便不予分配;
- SP(S, 1, 1):表示互斥信号量
- SP(S, 1, 0):可作为一个互控开关(当S>=1时,允许多个进程进入临界区;当S = 0时,禁止任何进程进入临界区)
6. 生产者-消费者问题(the producer-consumer problem)
当事件发生时,生产者线程创建一个事件对象,放入事件缓冲区,消费者线程从缓冲区取出事件,进行响应处理。
6.1 三个进程P1、P2、P3 互斥使用一个包含N(N>0)个单元的缓冲区。P1 每次用produce()生成一个正整数并用put()送入缓冲区某一个空单元中;P2 每次用getodd()从该缓冲区中取出一个奇数并用countodd()统计奇数个数;P3 每次用 geteven()从该缓冲区中取出一个偶数并用counteven()统计偶数个数。请用信号量机制实现这三个进程的同步与互斥活动,并说明所定义的信号量的含义。
typedef int semaphore;
semaphore mutex = 1; //用于实现临界区互斥
semaphore empty = N; //指示空缓冲块数目
semaphore odd = 0; //指示临界区奇数数目
semaphore even = 0; //指示临界区偶数数目
P1:
while (true) {
num = produce();
P(empty);
P(mutex);
put(num);
V(mutex);
if (num % 2 == 0) {
V(even);
} else {
V(odd);
}
}
P2:
while (true) {
P(odd);
P(mutex);
getodd();
countodd();
V(mutex);
V(empty);
}
P3:
while (true) {
P(even);
P(mutex);
geteven();
counteven();
V(mutex);
V(empty);
}
6.2 一个野人部落从一个大锅中一起吃炖肉,这个大锅一次可以存放 M 人份的炖肉。当野人们想吃的时候,如果锅中不空,他们就自助着从大锅中吃肉。如果大锅空了,他们就叫醒厨师,等待厨师再做一锅肉。
野人线程未同步的代码如下:
while (true){
getServingFromPot();
eat();
}
厨师线程未同步的代码如下:
while (true) {
putServingsInPot(M);
}
同步的要求是:当大锅空的时候,野人不能够调用getServingFromPot();仅当大锅为空的时候,大厨才能够调用putServingsInPot()
问题:请写出满足同步要求的野人线程和厨师线程的同步原语。
typedef int semaphore;
semaphore mutex = 1;
semaphore empty = M;
semaphore full = 0;
厨师:
void producer(void) {
while (true) {
SP(empty, M, M);
P(mutex);
putServingInPot();
V(mutex);
SV(full, M);
}
}
野人:
void consumer(void) {
while (true) {
P(full);
P(mutex);
getServingFromPot();
eat();
V(mutex);
V(empty);
}
}
6.3 系统中有多个生产者进程和消费者进程,共享用一个可以存 1000 个产品的缓冲区(初始为空),当缓冲区为未满时,生产者进程可以放入一件其生产的产品,否则等待;当缓冲区为未空时,消费者进程可以取走一件产品,否则等待。要求一个消费者进程从缓冲区连续取出 10 件产品后,其他消费者进程才可以取产品,请用信号量 P,V 操作实现进程间的互斥和同步,要求写出完整的过程;并指出所用信号量的含义和初值。
product buffer[1000];
semaphore empty = 1000; //指示空缓冲块数目
semaphore full = 0; //指示满缓冲块数目
semaphore mutex1 = 1; //用于实现临界区互斥和生产者之间的互斥
semaphore mutex2 = 1; //用于消费者之间的互斥
int i = 0;
int j = 0;
void producer(void) {
produce newProduct;
P(empty);
P(mutex1);
buffer[i] = newProduct;
i = (i + 1) % 1000;
V(mutex1);
V(full);
}
void consumer(void) {
P(mutex2);
for (int t = 0; t < 10; t++) {
P(full);
P(mutex1);
consume buffer[j];
j = (j + 1) % 100;
V(mutex1);
V(empty);
}
V(mutex2);
}
6.4 某银行有n个服务柜台。每个顾客进店后先取一个号,并且等待叫号。当一个柜台人员空闲下来时,就叫下一个号。试设计一个使柜台人员和顾客同步的算法。
int waiting_id = 0; //当前客户编号
semaphore waiting_mutex = 1; //对waiting_id互斥访问
int next_id = 0; //下一位要服务客户编号
semaphore next_mutex = 1; //对next_id互斥访问
Customer:
P(waiting_mutex);
waiting_id++;
V(waiting_mutex);
Server:
while (true) {
P(next_mutex);
P(waiting_id);
if (next_id < waiting_id) {
next_id++;
}
V(waiting_id);
V(next_mutex);
serve next_id;
}
6.5 设有一个可以装A、B两种物品的仓库,其容量无限大,但要求仓库中A、B两种物品的数量满足-M≤A物品数量-B物品数量≤N,其中M和N为正整数
,试用信号量和PV操作描述A、B两种物品的入库过程。
semaphore mutex = 1;
semaphore sa = N;
semaphore sb = M;
A:
while (true) {
P(sa);
P(mutex);
A产品入库;
V(mutex);
V(sb);
}
B:
while (true) {
P(sb);
P(mutex);
B产品入库;
V(mutex);
V(sa);
}
如果仓库容量有限,如何处理?
如果增加一个消费者,同时消费A和B两个物品,如何设计算法?
6.6 构造水分子(H20)问题(Berkeley OS 课程习题)
存在两种线程,一个线程提供氧原子O,一个线程提供氢原子H。为了构建水分子,我们需要使用barrier让线程同步从而构建水分子(H2O)。
当线程通过barrier,需要调用bond(形成化学键),需要保证构建同一个分子的线程调用bond。当氧原子线程到达barrier,而氢原子线程还没到达,需要等待氢原子;当1个氢原子到达而没有其他线程到达,需要等待1个氢原子1个氧原子。
oxygen = 0; //氧原子的计数器
hydrogen = 0; //氢原子的计数器
semaphore mutex = 1; //保护计数器的mutex
Barrier barrier(3); //3表示需要调用3次wait后barrier才开放
//3个线程调用bond后的同步点,之后允许下一个线程继续
semaphore oxyQueue = 0; //氧气线程等待的信号量
semaphore hydroQueue = 0; //氢气线程等待的信号量
//用在信号量上睡眠来模拟队列
P(oxyQueue) //表示加入队列
V(oxyQueue) //表示离开队列
Oxygen:
P(mutex);
oxygen += 1;
if (hydrogen >= 2) {
V(hydrogen);
V(hydrogen);
hydrogen -= 2;
V(oxyQueue);
oxygen -= 1;
} else {
V(mutex);
}
P(oxyQueue);
bond();
barrier.wait()
V(mutex); //构建H2O成功,当三个线程离开barrier时候,最后那个线程拿着mutex,虽然我们不知道哪个线程拿着mutex,但我们一定要释放一次。
Hydrogen:
P(mutex);
hydrogen += 1;
if (hydrogen >= 2 && oxygen >= 1) {
V(hydroQueue);
V(hydroQueue);
hydrogen -= 2;
V(oxyQueue);
oxygen -= 1;
} else {
V(mutex);
}
P(hydroQueue);
bond();
barrier.wait();
如果是H2O2应该怎么写?
7. 读者-写者问题(the readers-writers problem)
对共享资源的读写操作,任一时刻“写者”最多只允许一个,而“读者”则允许多个,“读-写”互斥,“写-写”互斥,“读-读”允许。读者算法的模式:第一个读线程加锁,最后一个读线程解锁。
- 给定读写序列:r1, w1, w2, r2, r3, w3...
- 读者优先:r1, r2, r3, w1, w2, w3...
- 写者优先:r1, w1, w2, w3, r2, r3...(一旦有写者,则后续读者必须等待,唤醒时优先考虑写者)
- 读写公平:r1, w1, w2, r2, r3, w3...
7.1 读者优先
int readers = 0 //记录临界区内读者的数目
semaphore mutex = 1 //保护对readers的访问
semaphore roomEmpty = 1 //对屋子的互斥访问,初值为1表示一个空屋子
Writer
P(roomEmpty);
write; //critical region
V(roomEmpty);
Reader
P(mutex);
readers = readers + 1;
if (readers == 1) {
P(roomEmpty);
}
V(mutex);
read; //critical region
P(mutex);
readers = readers - 1;
if (readers == 0) {
V(roomEmpty);
}
V(mutex);
增加一个限制条件:同时读的“读者”最多Rn个,mx
表示“允许写”,初值为1,L
表示“允许读者数目”,初值为Rn。
Writer
SP(mx, 1, 1; L, Rn, 0);
write
SV(mx, 1);
Reader
SP(L, 1, 1; mx, 1, 0);
read
SV(L, 1);
7.2 读写公平
7.2.1 闸机(Turnstile)
用信号量queue
和两个连续P(queue)
和V(queue)
组成。当queue
值为0,闸机关闭,任何进程不能进入;当queue
值为1,多个进程可以轮流排队通过。
7.2.2 具体实现
int readers = 0;
semaphore mutex = 1;
semaphore roomEmpty = 1;
semaphore turnstile = 1;
Writer
P(turnstile);
P(roomEmpty);
write; //critical region
V(turnstile);
V(roomEmpty);
Reader
P(turnstile);
V(turnstile);
P(mutex);
readers = readers + 1;
if (readers == 1) {
P(roomEmpty);
}
V(mutex);
read; //critical region
P(mutex);
readers = readers - 1;
if (readers == 0) {
V(roomEmpty);
}
V(mutex);
7.3 写者优先
int readers, writers = 0;
semaphore mutexW = 1; //保护对writes的访问
semaphore mutexR = 1; //保护对readers的访问
semaphore wMutex = 1; //当有读者或写者进行操作时,阻止其他写者进行操作
semaphore rMutex = 1; //当有写者进行操作时,阻止其它读者进行操作
semaphore MUTEX = 1; //在写者优先算法中,这个信号量很重要,目的是在rMutex上不允许建设长队列,我们可以设想一种情况:如果没有该信号量,则当写进程访问时,所有的读进程都等待在rMutex的队列上;当写进程结束访问,开始处理第一个读进程(readers++),但此时有写进程也准备访问,根据写者优先的要求,应该先执行写进程,但此时写进程会等待在rMutex的队列上,与准备访问(还没处理)的读进程(可能有成百上千个)共同竞争读进程释放的rMutex,结果可想而知。所以只允许一个进程在rMutex上排队,其他进程在等待rMutex之前,在MUTEX上排队。
Writer:
P(mutexW);
writes = writers + 1;
if (writes == 1) {
P(rMutex);
}
V(mutexW);
P(wMutex);
write; //critical region
V(wMutex);
P(mutexW);
writes = writes - 1;
if (writes == 0) {
V(rMutex);
}
V(mutexW);
Reader:
P(MUTEX);
P(rMutex);
P(mutexR):
readers = readers + 1;
if (readers == 1) {
P(wMutex);
}
V(mutexR):
V(rMutex);
V(MUTEX);
read; //critical region
P(mutexR);
readers = readers - 1;
if (readers == 0) {
V(wMutex);
}
V(mutexR);
8. 理发师问题
理发店有一位理发师、一把理发椅和n把供等候理发的乘客坐的椅子;如果没有顾客,理发师便在理发椅上睡觉,当一个顾客到来时,叫醒理发师;如果理发师正在理发时,又有顾客来到,则如果有空椅子可坐,就坐下来等待,否则就离开。
互斥访问资源:排队的顾客数(计数器waiting)
同步:顾客唤醒理发师、理发师唤醒下一位等待顾客
semaphore customers = 0; //等待理发的顾客
semaphore barbers = 0; //等待顾客的理发师
semaphore mutex = 1; //互斥访问waiting
int waiting = 0; //等待的顾客数(不包含正在理发的顾客)
int CHAIRS = 10;
Barber:
while (true) {
P(customers);
P(mutex);
waiting = waiting - 1;
V(mutex);
V(barbers);
cut hair();
}
Customer
P(mutex);
if (waiting < CHAIRS) {
waiting = waiting + 1;
V(mutex);
V(customers);
P(barbers);
get haircut();
} else {
V(mutex);
}
9. 哲学家进餐问题
5个哲学家围绕一张圆桌而坐,桌子上放着5支筷子,每两个哲学家之间放一支;哲学家必须拿到左右两只筷子才能吃饭。假设哲学家用 i=0~4 编号,筷子也是,哲学家 i 必须拿到筷子 i 和筷子 i + 1 才能进食。
-
需要满足条件:
- 不能死锁;
- 不能饿死;
- 不能只有一个哲学家进食(保证并发度)
-
方案:
-
至多只允许四个哲学家同时(尝试)进餐,以保证至少有一个哲学家能够进餐,最终总会释放出他所使用过的两支筷子,从而可使更多的哲学家进餐。
semaphore dinners = 4;
semaphore chopstick[] = {1, 1, 1, 1, 1};
Philosopher i :
while (true) {
think();
P(dinners);
P(chopstick[(i + 1) % 5]);
P(chopstick[i]);
eat();
V(chopstick[(i + 1) % 5]);
V(chopstick[i]);
V(dinners);
}
-
同时拿起两根筷子,否则不拿起。
semaphore chopstick[] = {1, 1, 1, 1, 1};
Philosopher i:
while (true) {
think();
SP(chopstick[(i + 1) % 5], chopstick[i]);
eat();
SV(chopstick[(i + 1) % 5], chopstick[i]);
}
-
对筷子进行编号,奇数号哲学家先拿左,再拿右;偶数号相反。
semaphore chopstick[] = {1, 1, 1, 1, 1};
Philosopher i:
while (true) {
if (i % 2 == 0) {
think();
P(dinners);
P(chopstick[(i + 1) % 5]);
P(chopstick[i]);
eat();
V(chopstick[(i + 1) % 5]);
V(chopstick[i]);
V(dinners);
} else {
think();
P(dinners);
P(chopstick[i]);
P(chopstick[(i + 1) % 5]);
eat();
V(chopstick[i]);
V(chopstick[(i + 1) % 5]);
V(dinners);
}
}
-
10. 其他问题
10.1 寿司店问题。假设一个寿司店有 5 个座位,如果你到达的时候有一个空座位,你可以立刻就坐。但是如果你到达的时候 5 个座位都是满的有人已经就坐,这就意味着这些人都是一起来吃饭的,那么你需要等待所有的人一起离开才能就坐。编写同步原语,实现这个场景的约束。
int emptyChairs = n; //空闲的座位数
semaphore chair = n; //信号量,表示空闲的座位数
semaphore mutex = 1; //保护对emptyChairs的互斥访问
semaphore queue = 1; //对等待顾客的互斥访问,每次只处理一个等待顾客
P(queue);
P(mutex);
if (emptyChairs == 0) {
V(mutex);
SP(chair, 5, 1);
} else {
V(mutex);
SP(chair, 1, 1);
}
P(mutex);
emptyChairs--;
V(mutex);
V(queue);
eat;
P(mutex);
emptyChairs++;
V(mutex);
SV(chair, 1);
10.2 搜索-插入-删除问题。三个线程对一个单链表进行并发的访问,分别进行搜索、插入和删除。搜索线程仅仅读取链表,因此多个搜索线程可以并发。插入线程把数据项插入到链表最后的位置;多个插入线程必须互斥防止同时执行插入操作。但是,一个插入线程可以和多个搜索线程并发执行。最后,删除线程可以从链表中任何一个位置删除数据。一次只能有一个删除线程执行;删除线程之间,删除线程和搜索线程,删除线程和插入线程都不能同时执行。
semaphore noSearch = 1; //表示临界区无搜索进程
semaphore noInsert = 1; //表示临界区无插入进程
semaphore iMutex = 1; //用于插入进程之间的互斥
int insertNum = 0; //表示插入进程的数目
semaphore mutexInsertNum = 1; //保护对insertNum的互斥访问
int searchNum = 0; //表示搜索进程的数目
semaphore mutexSearchNum = 1; //保护对searchNum的互斥访问
Delete:
P(noSearch);
P(noInsert);
delete();
V(noInsert);
V(noSearch);
Insert:
P(mutexInsertNum);
insertNum++;
if (insertNum == 1) {
P(noInsert);
}
V(mutexInsertNum);
P(iMutex);
insert();
V(iMutex);
P(mutexInsertNum);
insertNum--;
if (insertNum == 0) {
V(noInsert);
}
V(mutexInsertNum);
Search:
P(mutexSearchNum);
searchNum++;
if (searchNum == 1) {
P(noSearch);
}
V(mutexSearchNum);
search();
P(mutexSearchNum);
searchNum--;
if (searchNum == 0) {
V(noSearch);
}
V(mutexSearchNum);
以上代码有可能导致delete进程饿死,可以思考如何尽可能实现公平?
以上部分内容引自课件,如有侵权,请及时联系我删除!