读写互斥锁是如何工作的?



假设我在一个没有多读/单写互斥的线程框架中编程。我可以用以下方式实现它们的功能吗?

创建两个互斥锁:一个是递归的(锁计数),一个是读的,一个是写的。

写:

    获取二进制互斥锁
  • 等待直到递归互斥锁计数为零
  • 实际写
  • 释放二进制互斥锁

:

  • 获取二进制互斥锁(所以我知道写器不是活动的)
  • 递归互斥锁增量计数
  • 释放二进制互斥锁实际读
  • 递归互斥量递减计数

这不是作业。我没有接受过并发编程方面的正式培训,我正在努力掌握这些问题。如果有人能指出一个缺陷,说明不变量或提供一个更好的算法,我将非常高兴。一个好的参考,无论是在线或死树,也将感激。

以下内容直接摘自《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实现,但仅将其用作伪代码。在进行实际实现时,您必须小心语言的内存模型,否则您肯定会以头痛告终。作为一个例子,我认为readAcquiresreadReleaseswriter变量都必须在Java中声明为易失性,或者编译器可以自由地将它们优化出循环。这是因为在严格顺序的程序中,对一个在循环中从未改变过的变量进行连续循环是没有意义的。请注意,我的Java有点生疏,所以我可能是错的。还有另一个问题是readReleasesreadAcquires变量的整数溢出,这在算法中被忽略了。

在我解释这个算法之前最后一点说明。使用锁初始化条件变量。这意味着当一个线程调用condition.await()时,它放弃了对锁的所有权。一旦被调用condition.signalAll()唤醒,线程将在重新获得锁后恢复。

最后,这里是它的工作原理和原因。readReleasesreadAcquires变量跟踪已经获得和释放读锁的线程数。当它们相等时,没有线程具有读锁。writer变量表示线程正在尝试获取写锁,或者它已经拥有了写锁。

算法的读锁部分相当简单。当尝试锁定时,它首先检查写程序是否持有该锁或正在尝试获取该锁。如果是,它等待,直到写操作完成,然后通过增加readAcquires变量来为读操作声明锁。在解锁时,线程增加readReleases变量,如果没有更多的读取器,它会通知任何可能正在等待的写入器。

算法的写锁部分并不复杂。要锁定,线程必须首先检查是否有其他写入器处于活动状态。如果是,则必须等到另一个写入器完成。然后,它通过将writer设置为true来表明它想要锁(注意,它还没有持有它)。然后,它等待,直到没有更多的读者,然后继续。要解锁,它只需将变量writer设置为false,并通知任何其他可能正在等待的线程。

这个算法是公平的,因为读取器不能无限期地阻塞写入器。一旦写程序表明它想要获取锁,就没有更多的读程序可以获取锁。之后,作者只是等待最后剩下的读者读完,然后再继续。注意,仍然存在一个写入器无限期阻塞另一个写入器的可能性。这是一个相当罕见的情况,但算法可以改进以考虑到这一点。


所以我重读了你的问题,意识到我用下面给出的算法部分(糟糕地)回答了它。这是我的第二次尝试。

你描述的算法与我提到的书中提供的简单版本非常相似。唯一的问题是,A)这是不公平的,B)我不确定你将如何实现wait until recursive mutex has lock count zero。对于A)(见上文)和B),本书使用一个int来跟踪读者,并使用一个条件变量来执行信令。

  1. 您可能希望防止写饥饿,为了实现这一点,您可以优先考虑写操作或使互斥锁公平。
    ReadWriteLock Java的接口文档说Writer首选项是通用的
    ReentrantReadWriteLock类文档说该类不强制锁访问的读或写优先顺序。但是,它支持可选的公平性策略

  2. 注意R . .

    而不是锁定和解锁二进制互斥锁进行读取,您可以可以只是检查二进制互斥状态后增加计数递归互斥,如果是,等待(spin/yield/futex_wait/等等)锁定,直到被解锁

  3. 推荐阅读:
    用POSIX线程编程
    Perl的RWLock
    Java的ReadWriteLock文档

最新更新