假设我在一个没有multiple-reader/single-writer mutexes的线程框架中进行编程。我可以使用以下方法实现其功能:
创建两个互斥锁:一个用于读者的递归(锁定计数)和一个用于编写者的二进制缓冲区。
写:
读:
这不是作业。我没有正式的并发编程培训,我正在努力把握这些问题。如果有人可以指出一个缺陷,拼出不变量或提供更好的算法,我会非常高兴。在网上或死树上的一个很好的参考也将不胜感激。
答案 0 :(得分:20)
以下内容直接来自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实现,但只能将其用作伪代码。在进行实际实施时,您必须小心语言的memory model,否则您肯定会头疼。作为一个例子,我认为readAcquires
和readReleases
和writer
变量都必须在Java中声明为volatile,或者编译器可以自由地从循环中优化它们。这是因为在严格的顺序程序中,连续循环一个在循环内从未改变的变量是没有意义的。请注意,我的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来跟踪读者和条件变量来进行信号传递。
答案 1 :(得分:3)
您可能希望防止写入饥饿,为了实现此目的,您可以优先考虑写入或使互斥变得公平。
ReadWriteLock
Java's interface documentation说作家首选是常见的,
ReentrantReadWriteLock
class documentation说
此类不会强制执行锁定访问的读者或编写者首选项顺序。但是,它确实支持可选的公平政策。
注意R ..的评论
而不是锁定和解锁二进制互斥锁以供阅读 可以在递增计数后检查二进制互斥锁状态 递归互斥,等待(spin / yield / futex_wait / whatever)如果是的话 锁定直到它解锁
推荐阅读:
Programming with POSIX Threads
Perl's RWLock
Java's ReadWriteLock documentation