ReaderWriterLockSection: плохая идея? - PullRequest
4 голосов
/ 14 декабря 2009

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

Видя возможность избежать повторения и инкапсулировать это поведение, я создал класс ReaderWriterLockSection, предназначенный для использования в качестве тонкой оболочки для блокировки, которую можно использовать с синтаксисом блока C # using.

Класс в основном выглядит следующим образом:

public enum ReaderWriterLockType
{
   Read,
   UpgradeableRead,
   Write
}

public class ReaderWriterLockSection : IDisposeable
{
   public ReaderWriterLockSection(
       ReaderWriterLockSlim lock, 
       ReaderWriterLockType lockType)
   {
       // Enter lock.
   }

   public void UpgradeToWriteLock()
   {
       // Check lock can be upgraded.
       // Enter write lock.
   }

   public void Dispose()
   {
       // Exit lock.
   }
}

Я использую раздел следующим образом:

private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

public void Foo()
{
    using(new ReaderWriterLockSection(_lock, ReaderWriterLockType.Read)
    {
        // Do some reads.
    }
}

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

Кто-нибудь может увидеть проблему с этим подходом? Есть ли причина, по которой это плохая идея?

Ответы [ 4 ]

3 голосов
/ 14 декабря 2009

Ну, мне кажется, все в порядке. Эрик Липперт ранее писал об опасности использования Dispose для «нересурсных» сценариев, но я думаю, что это будет считаться ресурсом.

Это может усложнить жизнь в сценариях обновления, но вы всегда можете вернуться к более ручному фрагменту кода в этот момент.

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

1 голос
/ 04 марта 2012

Одна вещь, которую я бы посоветовал при обертывании блокировки, чтобы упростить шаблон «использования», - это включить в замок поле «опасное состояние»; прежде чем разрешить ввод кода в код, необходимо проверить состояние опасности. Если состояние опасности установлено, и код, который пытается войти в блокировку, не передал специальный параметр, говорящий, что он ожидает, что это может быть, попытка получить блокировку должна вызвать исключение. Код, который временно переводит защищенный ресурс в плохое состояние, должен установить флаг состояния опасности, сделать то, что необходимо сделать, а затем сбросить флаг состояния опасности после завершения операции и перехода объекта в безопасное состояние.

Если во время установки флага состояния опасности происходит исключение, блокировку следует снять, но флаг состояния опасности должен оставаться установленным. Это гарантирует, что код, который хочет получить доступ к ресурсу, обнаружит, что ресурс поврежден, вместо того, чтобы вечно ожидать снятия блокировки (что будет результатом, если не будет блоков «using» или «try-finally»). ).

Если блокируемой блокировкой является ReaderWriterLock, может оказаться удобным, чтобы получение блокировки «записи» автоматически устанавливало состояние опасности; к сожалению, для IDisposable, используемого блоком using, нет способа определить, был ли блок завершен корректно или через исключение. Следовательно, я не знаю никакого способа использовать что-либо синтаксически, например, блок «using» для защиты флага «состояние опасности».

1 голос
/ 14 декабря 2009

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

Ваш подход решает проблему, только если код, который читает или записывает общий ресурс, генерирует исключение. Неявным является то, что вы не обрабатываете исключение в методе, который выполняет чтение / запись. Если вы это сделали, вы можете просто снять блокировку в коде обработки исключений. Если вы вообще не обработаете исключение, поток умрет от необработанного исключения, и ваша программа прекратит работу. Нет смысла снимать блокировку.

Итак, где-то ниже в стеке вызовов есть предложение catch, которое перехватывает исключение и обрабатывает его. Этот код должен восстановить состояние программы, чтобы он мог продолжать работу без создания неверных данных или иным образом погибнуть из-за исключений, вызванных измененным состоянием. Этот код имеет сложную работу. Нужно как-то угадать, сколько данных было прочитано или записано без какого-либо контекста. Понимание этого неправильно или только частично правильно, потенциально очень дестабилизирует всю программу. В конце концов, это был общий ресурс, другие потоки читают / пишут с него.

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

1 голос
/ 14 декабря 2009

Я обычно балуюсь этим видом сладких кондитерских изделий!

Вот вариант, который немного легче читать пользователям, поверх вашего API

public static class ReaderWriterLockExt{
  public static IDisposable ForRead(ReaderWriterLockSlim rwLock){
    return new ReaderWriterLockSection(rwLock,ReaderWriterLockType.Read);
  }
  public static IDisposable ForWrite(ReaderWriterLockSlim rwLock){
    return new ReaderWriterLockSection(rwLock,ReaderWriterLockType.Write);
  }
  public static IDisposable ForUpgradeableRead(ReaderWriterLockSlim wrLock){
    return new ReaderWriterLockSection(rwLock,ReaderWriterLockType.UpgradeableRead);
  }
}

public static class Foo(){
  private static readonly ReaderWriterLockSlim l=new ReaderWriterLockSlim(); // our lock
  public static void Demo(){


    using(l.ForUpgradeableRead()){ // we might need to write..

      if(CacheExpires()){   // checks the scenario where we need to write

         using(l.ForWrite()){ // will request the write permission
           RefreshCache();
         } // relinquish the upgraded write
      }

      // back into read mode
      return CachedValue();
    } // release the read 
  }
}

Я также рекомендую использовать вариант, который использует делегат Action, который вызывается, когда блокировка не может быть получена в течение 10 секунд, что я оставлю читателю в качестве упражнения.

Возможно, вы также захотите проверить наличие нулевого RWL в методах статического расширения и убедиться, что блокировка существует при ее удалении.

Ура, Florian

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...