可以在逻辑上分为四个部分:
(“上锁” 其实就是占用一个资源,所以“锁”也可以理解为资源。)
信号量是一个变量(可以是一个整数,或者是更复杂的记录型变量,比如带有等待队列的),可以用一个信号量来表示系统中某种资源的数量。例如有一台打印机,可以设一个初值为 1 的信号量。
对信号量只能进行 wait
和 signal
操作,简称P、V 操作
。P 操作实现占用一个资源,V 操作实现释放一个资源。复杂型信号量可以实现当进程申请的资源不够时,让其挂在该信号量的等待队列上。P、V 都是原语,执行过程中不可以被中断,从而很方便的实现了进程互斥、同步。
下面给出信号量机制实现进程互斥、同步的参考步骤,不过具体步骤还是要根据问题来分析。
注意:1. 对不同的临界资源要设置不同的互斥信号量
2. P、V 操作必须成对出现
(3和4其实就是只有前者做完了他的事才可以让后者执行)
有读者和写者两组并发进程,共享一个文件,当两个或以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:
下面是四种解决方案,代码都是伪代码,缩进为一个代码块(pythonic)。
信号量都是有等待队列的类型,用semaphore
表示。其中对信号量的 V 操作的调度算法是 FIFO。
如何分析解决方案会导致什么样的问题,只要假设一下读写进程执行的顺序来验证就可以了。
根据上面的知识点,对读写者问题进行分析:
互斥关系:写进程之间、写进程和读进程之间需要互斥访问临界区
这个问题比较简单,一般书上都有。定义以下变量:
semaphore rw = 1;
实现读、写进程对文件的互斥访问semaphore mutex = 1;
实现互斥修改变量int readcnt = 0;
记录当前有几个读者在读写者进程
while(true):
P(rw) // 写之前占用临界区资源
/** 临界区写文件 **/
V(rw) // 写完后释放临界区资源
读者进程
while(true):
P(mutex)
if(readcnt==0) // 第一个读者进程负责占用临界区资源
P(rw)
readcnt+=1 // 访问临界区的读者进程数+1
V(mutex)
/** 临界区读文件 **/
P(mutex)
readcnt-=1 // 访问临界区的读者进程数-1
if(readcnt==0) // 最后一个读者进程负责释放临界区资源
V(rw)
V(mutex)
分析
这种方案是读进程优先,只要有读进程还在读,写进程就要一直阻塞在P(rw)
,可能会“饿死”。
有些教程可能会说这个是 “写优先”,实际上是一种相对公平的先来先服务算法。定义以下变量:
semaphore rw = 1;
实现读、写进程对文件的互斥访问semaphore w = 1;
实现“写优先”(实际上是“先来先服务”)semaphore mutex = 1;
实现互斥修改变量int readcnt = 0;
记录当前有几个读者在读写者进程
while(true):
P(w) // 读进程和写进程哪个先来到就会先拿到w资源
P(rw) // 写之前占用临界区资源
/** 临界区写文件 **/
V(rw) // 写完后释放临界区资源
V(w)
读者进程
while(true):
P(w) // 读进程和写进程哪个先来到就会先拿到w资源
P(mutex)
if(readcnt==0) // 第一个读者进程负责占用临界区资源
P(rw)
readcnt+=1 // 访问临界区的读者进程数+1
V(mutex)
V(w)
/** 临界区读文件 **/
P(mutex)
readcnt-=1 // 访问临界区的读者进程数-1
if(readcnt==0) // 最后一个读者进程负责释放临界区资源
V(rw)
V(mutex)
分析
相比于上一个解法,这个解法只在两个进程中多加了一对P、V
操作,而且都是每个新来的进程在进入区的时候就要执行P(w)
,就可以实现先来先服务。
不过读、写者还是要互斥访问临界区的,所以持有w
锁的读进程需要等前面已经在临界区的写进程完成后才可以执行,对于写进程持有w
锁时也一样要等前面的读进程完成读操作。
但虽然是先来先服务,不过还是可以说稍微提高了读者进程的优先级,因为写者可能需要等待多个读者,而读者最多只需要等待一个写者(原因就是读者不需要互斥访问临界区)。
下面这个算法是在网上还有看到同学写的 “写者优先” 的算法,但写者进程也是不能够完全抢占读者进程的。先来看看伪代码,定义以下变量:
semaphore rw = 1;
实现读、写进程对文件的互斥访问semaphore w = 1;
实现写进程之间对文件的互斥访问semaphore mutex1 = 1, mutex2 = 1;
实现互斥修改变量int readcnt = 0, writecnt = 0;
分别记录当前有几个读、写进程想访问临界区写者进程
while(true):
P(mutex1)
if(writecnt==0) // 第一个写者进程负责占用临界区资源
P(rw)
writecnt+=1 // 访问临界区的写者进程数+1
V(mutex1)
P(w) // 写之前占用临界区资源
/** 临界区写文件 **/
V(w) // 写完后释放临界区资源
P(mutex1)
writecnt-=1 // 访问临界区的写者进程数-1
if(writecnt==0) // 最后一个写者进程负责释放临界区资源
V(rw)
V(mutex1)
读者进程(和方案1一样)
while(true):
P(mutex2)
if(readcnt==0) // 第一个读者进程负责占用临界区资源
P(rw)
readcnt+=1 // 访问临界区的读者进程数+1
V(mutex2)
/** 临界区读文件 **/
P(mutex2)
readcnt-=1 // 访问临界区的读者进程数-1
if(readcnt==0) // 最后一个读者进程负责释放临界区资源
V(rw)
V(mutex2)
分析
当写者拿到rw
锁时,后来的写者都可以直接等待在P(w)
处(在临界区外排队等待进入),而后来的读者中有一个会阻塞在P(rw)
,剩余的阻塞在P(mutex2)
,这样一来读者也需要等待多个写者了,而方案二中读者最多只需要等一个写者。
这时的情况像是读、写进程是两个派别,谁先第一个拿到rw
锁,其派别的 “兄弟们” 都能跟着他一起上。没拿到锁的一方只能等对方所有人完成后才能执行。
因此有多个读者访问临界区时,写者也只能等待,而我们希望的是写者能够抢占读者进程,不需要等后来的读者。所以这种方案也只能说是再一步提高了写进程的优先级。
上一个方案不能实现完全的写者优先的原因是每次只让第一个来到的读者进行了“检查”,后序读者就不用判断了。那只要能够实现每个来到进入区的读者都要检查是不是有写者想访问临界区就行了。定义以下变量:
semaphore rw = 1;
实现对文件的互斥访问semaphore mutex1 = 1, mutex2 = 1;
实现互斥修改变量semaphore mutex3 = 1;
保证只有一个读者进程会被卡在 P( read )semaphore read = 0;
同步锁:读进程必须在没有写进程时才能访问临界区int readcnt = 0, writecnt = 0;
分别记录当前有几个读进程在读、有几个写进程想写bool waiting = false;
表示当有写进程在写文件时,有没有读进程来到写者进程
while(true):
P(mutex1)
writecnt+=1 // 只要有读进程来了,writecnt+1
V(mutex1)
P(rw) // 写之前占用临界区资源
/** 临界区进行写 **/
V(rw) // 写完后释放临界区资源
P(mutex1)
writecnt-=1 // 写完了writecnt-1
// 没有写者想写 且 有读者正阻塞在P(read) 就释放read让读者读文件
if(writecnt==0 && waiting):
V(read)
waiting = false
// 如果没有waiting,可能导致没有读者时写者会V(read)导致read资源变多
V(mutex1)
读者进程
while(true):
P(mutex3) // 如果没有mutex3,可能造成多个读进程阻塞在P(read)
if(writecnt!=0): // 一旦有写者来了,后来的读者就要等待写者写完
waiting = true // 表明现在有读者在等写者
P(read) // 要等写者写完才可以继续
V(mutex3)
P(mutex2)
if(readcnt==0): // 第一个读者进程负责占用临界区资源
p(rw)
readcnt+=1
V(mutex2)
/** 临界区进行读 **/
P(mutex2)
readcnt-=1
if(readcnt==0): // 最后一个读者进程负责释放临界区资源
V(rw)
V(mutex2)
分析
读者1 → 写者 → 读者2:假设读者1占有rw
资源,写者来到阻塞在P(rw)
处,此时 writecnt
已经不是 0。所以读者2 再来时会被阻塞在P(read)
处。当读者1 完成读操作释放rw
锁,然后写者立即会占有rw
,此时一直有写者来的话都是可以直接在临界区外排队的。等所有写操作完成了,writecnt
就变为 0,读者才能获得read
锁进行读。
在读者先于写者来到的情况下,写者总是会获得rw
锁,因为当有写者在等待rw
时,其它后来的读者中的第一个会阻塞在P(read)
,剩余的阻塞在P(mutex3)
。而前面已经在临界区的读者,写完后就会有readcnt==0
然后释放rw
锁,写者立刻就可以访问临界区了。但是在方案三的话,因为后来的读者不会进行 “检查”,只要它们中有一个读者持有rw
锁,即使已经有写者正在等待,读者们依然可以直接进入临界区,但在方案四中却会被阻塞在P(mutex3)
或P(read)
。