Как работает мьютекс/блокировка чтения/записи?

Скажем, я программирую в инфраструктуре потоков, которая не имеет мультезы с несколькими читателями/одиночными писателями. Могу ли я реализовать их функциональные возможности со следующим:

Создайте два мьютекса: рекурсивный (блокировка) для чтения и двоичный для писателя.

Запись:

  • получить блокировку двоичных мьютексов
  • до тех пор, пока рекурсивный мьютекс не будет блокировать число 0
  • фактическая запись
  • блокировка блокировки двоичных мьютексов

Чтение:

  • получить блокировку двоичных мьютексов (поэтому я знаю, что сценарий неактивен)
  • количество приращений рекурсивного мьютекса
  • блокировка блокировки двоичных мьютексов
  • актуальное чтение
  • количество декрементов рекурсивного мьютекса

Это не домашнее задание. У меня нет формального обучения в параллельном программировании, и я пытаюсь понять проблемы. Если кто-то может указать на недостаток, изложить инварианты или обеспечить лучший алгоритм, я был бы очень доволен. Также будет оценена хорошая рекомендация, будь то онлайн или мертвые деревья.

Ответы

Ответ 1

Следующее взято непосредственно из Искусства Многопроцессорного Программирования, который является хорошей книгой, чтобы узнать об этом материале. На самом деле представлено 2 реализации: простая версия и верная версия. Я пойду дальше и воспроизведу честную версию.

Одним из требований для этой реализации является наличие примитива переменной условия. Я попытаюсь найти способ удалить его, но это может занять некоторое время. До тех пор это все равно должно быть лучше, чем ничего. Обратите внимание, что также возможно реализовать этот примитив, используя только блокировки.

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 отслеживают количество потоков, которые получили и readAcquires блокировку чтения. Когда они равны, ни один поток не имеет блокировки чтения. Переменная writer указывает, что поток пытается получить блокировку записи или уже имеет ее.

Часть алгоритма блокировки чтения довольно проста. При попытке блокировки он сначала проверяет, удерживает ли писатель блокировку или пытается ее получить. Если это так, он ждет, пока не readAcquires и затем readAcquires блокировку для читателей, увеличивая переменную readAcquires. При разблокировке поток увеличивает переменную readReleases и, если читателей больше нет, он уведомляет всех авторов, которые могут ожидать.

Часть алгоритма блокировки записи не намного сложнее. Чтобы заблокировать, поток должен сначала проверить, активен ли другой писатель. Если они есть, он должен ждать, пока другой писатель не закончит. Затем он указывает на то, что он хочет, чтобы блокировку, установив writer к истинным (обратите внимание, что он не держит его еще). Затем он ждет, пока не останется больше читателей, прежде чем продолжить. Чтобы разблокировать, он просто устанавливает переменную writer в false и уведомляет любые другие потоки, которые могут ожидать.

Этот алгоритм справедлив, потому что читатели не могут блокировать писателя на неопределенный срок. Как только автор указывает, что хочет получить блокировку, больше читатели не смогут получить блокировку. После этого писатель просто ждет, пока последние оставшиеся читатели закончат, прежде чем продолжить. Обратите внимание, что существует возможность того, что писатель на неопределенный срок блокирует другого писателя. Это довольно редкий случай, но алгоритм может быть улучшен, чтобы принять это во внимание.


Поэтому я перечитал ваш вопрос и понял, что частично (плохо) ответил на него по алгоритму, представленному ниже. Итак, моя вторая попытка.

Описанный вами алгоритм довольно похож на простую версию, представленную в упомянутой мной книге. Единственная проблема в том, что A) это не честно и B) я не уверен, как бы вы реализовали wait until recursive mutex has lock count zero. Для A), см. Выше и для B), книга использует единственное int для отслеживания читателей и переменную условия для выполнения сигнализации.

Ответ 2

  1. Возможно, вы захотите предотвратить голодание при записи, для этого вы можете либо отдать предпочтение записи, либо сделать мьютекс справедливым.
    Документация интерфейса Java ReadWriteLock говорит, что предпочтение Writer распространено,
    ReentrantReadWriteLock документации класса ReentrantReadWriteLock говорится, что этот класс не навязывает читателю или читателю порядок упорядочения для доступа к блокировке. Тем не менее, он поддерживает факультативную политику справедливости.

  2. Примечание R.. комментарий

    Вместо того, чтобы блокировать и разблокировать бинарный мьютекс для чтения, вы можете просто проверить состояние бинарного мьютекса после увеличения счетчика рекурсивного мьютекса и подождать (spin/yield/futex_wait/что угодно), если он заблокирован, до тех пор, пока он не станет разблокированным.

  3. Рекомендуемое чтение:
    Программирование с помощью POSIX Threads
    Perl RWLock
    Документация Java ReadWriteLock.