ReaderWriterLockSlim.EnterUpgradeableReadLock() Всегда тупик?

Я очень хорошо знаком с ReaderWriterLockSlim, но недавно попытался реализовать EnterUpgradeableReadLock() в классе... Вскоре после того, как я понял, что это почти наверняка гарантированный тупик, когда два или более потока запускают код:

Thread A --> enter upgradeable read lock
Thread B --> enter upgradeable read lock
Thread A --> tries to enter write lock, blocks for B to leave read
Thread B --> tries to enter write lock, blocks for A to leave read
Thread A --> waiting for B to exit read lock
Thread B --> waiting for A to exit read lock

Что мне здесь не хватает?

ИЗМЕНИТЬ

Добавлен пример кода моего сценария. Метод Run() будет вызываться двумя или более потоками одновременно.

public class Deadlocker
{
    private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

    public void Run()
    {
        _lock.EnterUpgradeableReadLock();
        try
        {
            _lock.EnterWriteLock();
            try
            {
                // Do something
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();
        }
    }
}

Ответ 1

Долгое время после OP, но я не согласен с принятым в настоящее время ответом.

Утверждение Thread B --> enter upgradeable read lock неверно. Из документов

Только один поток может быть в обновленном режиме в любое время

И в ответ на ваши комментарии: он предназначен для совершенно другого использования для шаблона Read-Write.

TL; DR. Модернизированный режим полезен:

  • если автор должен проверить общий ресурс перед его записью и (необязательно) должен избегать условий гонки с другими авторами;
  • и он не должен останавливать читателей, пока не будет на 100% уверен, что он должен писать на общий ресурс;
  • и вполне вероятно, что автор решит, что он не должен писать на общий ресурс после выполнения проверки.

Или, в псевдокоде, где это:

// no other writers or upgradeables allowed in here => no race conditions
EnterUpgradeableLock(); 
if (isWriteRequired()) { EnterWriteLock(); DoWrite(); ExitWriteLock(); } 
ExitUpgradeableLock();

дает "лучшую производительность" ÷ чем это:

EnterWriteLock(); if (isWriteRequired()) { DoWrite(); } ExitWriteLock();

Его следует использовать с осторожностью, если эксклюзивные блокировки занимают очень много времени из-за использования SpinLock.


Аналогичная конструкция блокировки

Блокировка с обновлением на удивление похожа на SQL-сервер Блокировка SIX (Shared with Intent to go eXclusive) .

  • Чтобы переписать вышеприведенное утверждение в этих терминах, блокировка Upgradeable говорит: "Писатель намерен писать ресурсу, но хочет поделиться им с другими читателями, пока он [double] проверяет состояние, чтобы увидеть, должно ли оно eXclusive блокировать и выполните запись" .

Без существования блокировки Intent вы должны выполнить проверку "должен ли я сделать это изменение" внутри блокировки eXclusive, которая может повредить concurrency.

Почему вы не можете делиться Upgradeable?

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

Пример

Если мы просмотрим все события ожидания ожидания/входа/выхода блокировки как последовательные, а работа внутри блокировки будет параллельной, тогда мы можем написать сценарий в форме "Мрамор" (e enter; w wait; x exit; cr проверить ресурс; mr мутировать ресурс; R Shared/Read; U Intent/Upgradeable; w eXclusive/Write):

1--eU--cr--wW----eW--mr--xWxU--------------
2------eR----xR----------------eR--xR------
3--------eR----xR--------------------------
4----wU----------------------eU--cr--xU----

В словах: T1 входит в блокировку Upgradeable/Intent. T4 ждет блокировки Upgradeable/Intent. T2 и T3 вводят блокировки чтения. T1 тем временем проверяет ресурс, выигрывает гонку и ждет блокировки eXclusive/Write. T2 & T3 выходят из своих замков. T1 входит в блокировку eXclusive/Write и вносит изменения. T4 входит в блокировку Upgradeable/Intent, не требует внесения изменений и выходов, не блокируя T2, который еще раз читает.

В 8 пунктах...

Блокировка с обновлением:

  • используемый любым Writer;
  • который, скорее всего, сначала проверит, а затем решит не выполнять запись по какой-либо причине (потеря состояния гонки или шаблон Getsert);
  • и кто не должен блокировать чтение, пока не узнает, что он должен выполнить запись;
  • после чего он выберет эксклюзивную блокировку и сделает это.

Обновление не требуется, если выполнено одно из следующих действий (включая, но не только):

  1. Конфликты между читателями и писателями, которые writelock-check-nowrite-exit равны нулю (проверка состояния записи выполняется очень быстро), т.е. конструкция Upgradeable не помогает производительности чтения,
  2. Вероятность наличия записи, которая записывается один раз в блокировку записи, равна ~ 1, потому что либо:

    • ReadLock-Check-WriteLock-DoubleCheck настолько быстро, что он только заставляет проиграть гонку один раз за триллион записей;
    • все изменения уникальны (все изменения должны произойти, расы не могут существовать); или
    • "последнее изменение выигрывает" (все изменения все равно должны произойти, хотя они не уникальны)

Это также не требуется, если a lock(...){...} является более подходящим, то есть:

  1. вероятность перекрытия окон чтения и/или записи низкая (блокировки могут быть как можно меньше предотвращать очень редкие события, защищая очень вероятные события, не говоря уже о простых требованиях к памяти)
  2. Все ваши блокировки становятся обновляемыми или пишутся, никогда не читаются ('duh')

÷ Где "производительность" зависит от вас, чтобы определить

Если вы рассматриваете объект блокировки как таблицу и защищенные ресурсы как ресурсы ниже в иерархии, эта аналогия приблизительно содержит

Первоначальная проверка блокировки чтения будет необязательной, проверка внутри блокировки обновления является обязательной, поэтому ее можно использовать в шаблоне с одиночной или двойной проверкой.

Ответ 2

Я рекомендую избегать EnterUpgradableReadLock(). Просто используйте EnterWriteLock(). Я знаю, что это кажется неэффективным, обновляемая блокировка чтения почти так же плоха, как блокировка записи.

Ответ 3

У вас есть ошибка в вашем примере

private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

он должен быть

private static readonly ReaderWriterLockSlim _lock = new  ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

Теперь в вашем коде каждый раз, когда выполняется экземпляр класса, он создает новый экземпляр ReaderWriterLockSlim, который не может заблокировать что-либо, потому что каждый отдельный поток имеет свой собственный экземпляр. Создание статики заставит все потоки использовать один экземпляр, который будет работать так, как должен