在《Multithreading Applications in Win32》(Jim Beveridge & Robert Wiener)一书中提及读写锁的实现时,作者阐述尝试了网络上近乎全部的实现发现均存在问题,后来参考一篇已发表的文章《Concurrent Control with “Readers” and “Writers”》使用Win32基础同步机制实现了读写锁。由于Windows自Vista版本开始才有读写锁的支持,此前笔者在WinXP上想使用读写锁时遇到困扰,遇到上述文章,确有醍醐灌顶的味道,现尝试翻译下,增加下自己的理解。这篇文章于1971年在“Operating Systems”上发表的论文,线程为80年代出现的概念,所以论文中使用进程讨论多进程同时访问临界区域的问题。
多进程同时访问临界区域(critical section)的问题可以看成两类进程:读者(readers)和写者(writers)。多个读者之间可以共享临界区域,但是多个写者之间必须互斥的访问临界区域。所以使用基本同步机制时有两种情况需要解决:当读者之间访问临界区域时,需要尽可能降低由基本同步机制带来的时间延迟;当写者出现时,相对与读者需要尽可能早的访问临界区域。
关键字:互斥锁,临界区域,共享访问资源
Dijkstra、Knuth和De Bruijn都讨论过操作系统必须保证对共享资源的互斥访问的问题,他们使用“P(sem–)”和“V(sem++)”操作简单的解决这个问题。我们认识到实际上访问共享资源的进程有两类:第一类进程叫“写者(writers)”,此类进程之间直接必须互斥的访问共享资源。第二类进程叫“读者(readers)”,此类进程之间可以同时访问共享资源。
情况一
对于读者(readers),我们要求自己的解决方案要满足读者只有在写者(writers)已经获取共享资源后才会进入等待状态,当写者正在等待一部分读者结束对共享资源的访问时,另一部分已经结束对共享资源访问的读者也不应该进入等待状态 。解决这种情况好像十分简单,但是经验告诉我们这并不容易。现有的众多解决方案过于复杂,我们讨论之后提出以下简化的解决方案,希望碰到相似问题的人可以少走些弯路。结局方案见Fig. 1。
Fig. 1
integer readcount; (initial value = 0)
semaphore mutex, w; (initial value for both = 1)
READER | WRITER |
---|---|
P(mutex); readcount = readcount + 1; if readcount == 1 then P(w); V(mutex); | P(w); |
…reading is performed … | …writing is performed … |
P(mutex); readcount = readcount - 1; if readcount == 0 then V(w); V(mutex); | V(w); |
注意w
变量是一个互斥信号量(mutual exclusion semaphore),用于互斥读者与写者同时对临界区域的访问。对于读者而言,只在“第一个读者进入”和“最后一个读者离开”临界区域时才修改它,而被其他的读者和写者所忽略。mutex
用于确保同一时刻只有一个负责调节w
的读者存在。只有当所有读者和所以写者均未访问临界区域时,w
才为正值。
情况二
对于写者(writers),要求任一写者必须和所有的读者互斥的访问共享资源,另外我们增加了一个要求,一旦写者准备访问共享资源,要尽可能快的获取对共享资源的访问操作。这种情况的解决方案不同于“情况一”,因为增加的要求,当一个写者准备访问共享资源时,已经结束对共享资源访问的读者必须等待写者完成对共享资源的访问,即使这时的写者处于等待其他读者完成访问操作的过程中。对于“情况一”而言,当不停的存在读者访问共享资源时,写者有可能处于一直无限等待的过程中。我们提出的解决方案是优先让写者访问共享资源,且当存在写者访问共享资源时,允许读者无限的等待写者完成访问。原则上,解决方案应该让写者优先访问共享资源,而不是假设读者会进入到V
操作。另外,当多个进程等待一个信号量时,我们也无法预测在V
操作结束时哪一个进程将被执行。我们提出的解决方案见Figure 2。
Fig. 2
integer readcount, writecount; (initial value = 0)
semaphore mutex1, mutex2, mutex3, w, r; (initial value = 1)
READER | WRITER |
---|---|
P(mutex3); P(r); P(mutex1); readcount = readcount + 1; if readcount == 1 then P(w); V(mutex1); V(r); V(mutex3); | P(mutex2); writecount = writecount + 1; if writecount == 1 then P(r); V(mutex2); P(w); |
…reading is done … | …writing is performed … |
P(mutex1); readcount = readcount - 1; if readcount == 0 then V(w); V(mutex1); | V(w); P(mutex2); writecount = writecount - 1; if writecount == 0 then V(r); V(mutex2); |
很容易留意到,Fig. 2中的mutex1
he w
和Fig. 1中的mutex
和w
完全对应。r
信号量的临界区域(critical section)的行为与Fig.1中w
保护共享资源(shared resource)的行为一致,第一个写者操作P(r)
用来锁住读者的mutex1
和w
区域。mutex2
被用于写者的作用类似与Fig.1中mutex被用于读者的作用。mutex3
的存在是必要的,因为我们要最大程度保证写者的优先性质。如果没有mutex3
,当出现一个写者和一个以上的读者同时等待V(r)
被某个读者完成的状态时,我们不能保证写者的优先性质。mutex3
保证了读者互斥的访问“P(r)
”和“V(r)
”之间的代码,使得最多存在一个等待r
的进程,V
操作的结果是明确的。
结语
以上的解决方案不适用于写者为“FIFO discipline”的领域。为了提供这样的支持,我们必须进一步改善V
操作,或者使用信号量数组来表示写者的数量。
我们感谢卡内基梅隆大学(Carnegie-Mellon University)的A.N. Habermann在这个论文的早期版本中指出了一个错误。