假设我在一个没有多读/单写互斥的线程框架中编程。我可以用以下方式实现它们的功能吗?
创建两个互斥锁:一个是递归的(锁计数),一个是读的,一个是写的。
写:
- 获取二进制互斥锁
- 等待直到递归互斥锁计数为零 实际写释放二进制互斥锁
:
- 获取二进制互斥锁(所以我知道写器不是活动的)
- 递归互斥锁增量计数 释放二进制互斥锁实际读
- 递归互斥量递减计数
这不是作业。我没有接受过并发编程方面的正式培训,我正在努力掌握这些问题。如果有人能指出一个缺陷,说明不变量或提供一个更好的算法,我将非常高兴。一个好的参考,无论是在线或死树,也将感激。
以下内容直接摘自《The Art of Multiprocessor Programming》,这是一本学习这些内容的好书。实际上有两个实现:一个简单的版本和一个公平的版本。我将继续复制一个公平的版本。
这个实现的要求之一是你有一个条件变量原语。我会想办法把它去掉,但可能要花点时间。在那之前,有总比没有好。注意,也可以只使用锁来实现这个原语。
public class FifoReadWriteLock {
int readAcquires = 0, readReleases = 0;
boolean writer = false;
ReentrantLock lock;
Condition condition = lock.newCondition(); // This is the condition variable.
void readLock () {
lock.lock();
try {
while(writer)
condition.await();
readAcquires++;
}
finally {
lock.unlock();
}
}
void readUnlock () {
lock.lock();
try {
readReleases++;
if (readAcquires == readReleases)
condition.signalAll();
}
finally {
lock.unlock();
}
}
void writeLock () {
lock.lock();
try {
while (writer)
condition.await();
writer = true;
while (readAcquires != readReleases)
condition.await();
}
finally {
lock.unlock();
}
}
void writeUnlock() {
writer = false;
condition.signalAll();
}
}
首先,我稍微简化了代码,但算法保持不变。在这本书中,这个算法碰巧也有一个错误,在勘误表中得到了纠正。如果你打算读这本书,把勘误表放在身边,否则你会很困惑(就像我几分钟前试图重新理解算法时一样)。请注意,从好的方面来看,这是一件好事,因为它使您保持警觉,这是处理并发性时的要求。
接下来,虽然这可能是Java实现,但仅将其用作伪代码。在进行实际实现时,您必须小心语言的内存模型,否则您肯定会以头痛告终。作为一个例子,我认为readAcquires
和readReleases
和writer
变量都必须在Java中声明为易失性,或者编译器可以自由地将它们优化出循环。这是因为在严格顺序的程序中,对一个在循环中从未改变过的变量进行连续循环是没有意义的。请注意,我的Java有点生疏,所以我可能是错的。还有另一个问题是readReleases
和readAcquires
变量的整数溢出,这在算法中被忽略了。
在我解释这个算法之前最后一点说明。使用锁初始化条件变量。这意味着当一个线程调用condition.await()
时,它放弃了对锁的所有权。一旦被调用condition.signalAll()
唤醒,线程将在重新获得锁后恢复。
最后,这里是它的工作原理和原因。readReleases
和readAcquires
变量跟踪已经获得和释放读锁的线程数。当它们相等时,没有线程具有读锁。writer
变量表示线程正在尝试获取写锁,或者它已经拥有了写锁。
算法的读锁部分相当简单。当尝试锁定时,它首先检查写程序是否持有该锁或正在尝试获取该锁。如果是,它等待,直到写操作完成,然后通过增加readAcquires
变量来为读操作声明锁。在解锁时,线程增加readReleases
变量,如果没有更多的读取器,它会通知任何可能正在等待的写入器。
算法的写锁部分并不复杂。要锁定,线程必须首先检查是否有其他写入器处于活动状态。如果是,则必须等到另一个写入器完成。然后,它通过将writer
设置为true来表明它想要锁(注意,它还没有持有它)。然后,它等待,直到没有更多的读者,然后继续。要解锁,它只需将变量writer
设置为false,并通知任何其他可能正在等待的线程。
这个算法是公平的,因为读取器不能无限期地阻塞写入器。一旦写程序表明它想要获取锁,就没有更多的读程序可以获取锁。之后,作者只是等待最后剩下的读者读完,然后再继续。注意,仍然存在一个写入器无限期阻塞另一个写入器的可能性。这是一个相当罕见的情况,但算法可以改进以考虑到这一点。
所以我重读了你的问题,意识到我用下面给出的算法部分(糟糕地)回答了它。这是我的第二次尝试。
你描述的算法与我提到的书中提供的简单版本非常相似。唯一的问题是,A)这是不公平的,B)我不确定你将如何实现wait until recursive mutex has lock count zero
。对于A)(见上文)和B),本书使用一个int来跟踪读者,并使用一个条件变量来执行信令。
-
您可能希望防止写饥饿,为了实现这一点,您可以优先考虑写操作或使互斥锁公平。
ReadWriteLock
Java的接口文档说Writer首选项是通用的,ReentrantReadWriteLock
类文档说该类不强制锁访问的读或写优先顺序。但是,它支持可选的公平性策略 -
注意R . .
而不是锁定和解锁二进制互斥锁进行读取,您可以可以只是检查二进制互斥状态后增加计数递归互斥,如果是,等待(spin/yield/futex_wait/等等)锁定,直到被解锁
-
推荐阅读:
用POSIX线程编程
Perl的RWLock
Java的ReadWriteLock文档