读写互斥锁/锁如何工作?

时间:2011-04-14 18:24:58

标签: multithreading language-agnostic concurrency locking mutex

假设我在一个没有multiple-reader/single-writer mutexes的线程框架中进行编程。我可以使用以下方法实现其功能:

创建两个互斥锁:一个用于读者的递归(锁定计数)和一个用于编写者的二进制缓冲区。

写:

  • 获取对二进制互斥锁的锁定
  • 等到递归互斥锁具有锁定计数零
  • 实际写作
  • 释放对二进制互斥锁的锁定

读:

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

这不是作业。我没有正式的并发编程培训,我正在努力把握这些问题。如果有人可以指出一个缺陷,拼出不变量或提供更好的算法,我会非常高兴。在网上或死树上的一个很好的参考也将不胜感激。

2 个答案:

答案 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,否则您肯定会头疼。作为一个例子,我认为readAcquiresreadReleaseswriter变量都必须在Java中声明为volatile,或者编译器可以自由地从循环中优化它们。这是因为在严格的顺序程序中,连续循环一个在循环内从未改变的变量是没有意义的。请注意,我的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 :(得分:3)

  1. 您可能希望防止写入饥饿,为了实现此目的,您可以优先考虑写入或使互斥变得公平。
    ReadWriteLock Java's interface documentation作家首选是常见的
    ReentrantReadWriteLock class documentation此类不会强制执行锁定访问的读者或编写者首选项顺序。但是,它确实支持可选的公平政策。

  2. 注意R ..的评论

      

    而不是锁定和解锁二进制互斥锁以供阅读   可以在递增计数后检查二进制互斥锁状态   递归互斥,等待(spin / yield / futex_wait / whatever)如果是的话   锁定直到它解锁

  3. 推荐阅读:
    Programming with POSIX Threads
    Perl's RWLock
    Java's ReadWriteLock documentation