"используя" заявление против "попробуй наконец"

У меня есть куча свойств, которые я собираюсь использовать для блокировки чтения / записи. Я могу реализовать их либо с try finally или using пункт.

в try finally Я бы приобрел замок до того, как tryи выпустить в finally, в using В этом случае я хотел бы создать класс, который получает блокировку в своем конструкторе и освобождает его в методе Dispose.

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

Способ 1 (try finally):

static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
private DateTime dtMyDateTime_m
public DateTime MyDateTime
{
    get
    {
        rwlMyLock_m .AcquireReaderLock(0);
        try
        {
            return dtMyDateTime_m
        }
        finally
        {
            rwlMyLock_m .ReleaseReaderLock();
        }
    }
    set
    {
        rwlMyLock_m .AcquireWriterLock(0);
        try
        {
            dtMyDateTime_m = value;
        }
        finally
        {
            rwlMyLock_m .ReleaseWriterLock();
        }
    }
}

Способ 2:

static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
private DateTime dtMyDateTime_m
public DateTime MyDateTime
{
    get
    {
        using (new ReadLock(rwlMyLock_m))
        {
            return dtMyDateTime_m;
        }
    }
    set
    {
        using (new WriteLock(rwlMyLock_m))
        {
            dtMyDateTime_m = value;
        }
    }
}

public class ReadLock : IDisposable
{
    private ReaderWriterLock rwl;
    public ReadLock(ReaderWriterLock rwl)
    {
        this.rwl = rwl;
        rwl.AcquireReaderLock(0);
    }

    public void Dispose()
    {
        rwl.ReleaseReaderLock();
    }
}

public class WriteLock : IDisposable
{
    private ReaderWriterLock rwl;
    public WriteLock(ReaderWriterLock rwl)
    {
        this.rwl = rwl;
        rwl.AcquireWriterLock(0);
    }

    public void Dispose()
    {
        rwl.ReleaseWriterLock();
    }
}

15 ответов

Решение

Из MSDN, используя Statement (C# Reference)

Оператор using гарантирует, что Dispose вызывается, даже если возникает исключение, когда вы вызываете методы объекта. Вы можете достичь того же результата, поместив объект в блок try, а затем вызвав Dispose в блоке finally; на самом деле, именно так оператор using переводится компилятором. Ранее приведенный пример кода расширяется до следующего кода во время компиляции (обратите внимание на дополнительные фигурные скобки, чтобы создать ограниченную область видимости для объекта):

{
  Font font1 = new Font("Arial", 10.0f);
  try
  {
    byte charset = font1.GdiCharSet;
  }
  finally
  {
    if (font1 != null)
      ((IDisposable)font1).Dispose();
  }
}

Таким образом, в основном, это тот же код, но с хорошими автоматическими проверками нуля и дополнительной областью действия для вашей переменной. В документации также говорится, что она "обеспечивает правильное использование объекта IDisposable", поэтому вы можете получить еще лучшую поддержку фреймворка для любых неясных случаев в будущем.

Итак, перейдите к варианту 2.

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

Я определенно предпочитаю второй метод. Это более лаконично с точки зрения использования и менее подвержено ошибкам.

В первом случае, кто-то, редактирующий код, должен быть осторожен, чтобы ничего не вставить между вызовом блокировки Acquire(Read|Write) и попыткой.

(Использование блокировки чтения / записи для отдельных свойств, таких как это, обычно излишне. Однако их лучше применять на гораздо более высоком уровне. Простая блокировка здесь часто будет достаточной, поскольку вероятность конфликта, по-видимому, очень мала, учитывая время удержания блокировки и получение блокировки чтения / записи является более дорогой операцией, чем простая блокировка).

Рассмотрим возможность того, что оба решения плохие, потому что они маскируют исключения.

try без catch очевидно, должна быть плохая идея; см. MSDN, почему using Заявление также опасно.

Обратите внимание, что Microsoft теперь рекомендует ReaderWriterLockSlim вместо ReaderWriterLock.

Наконец, обратите внимание, что примеры Microsoft используют два блока try-catch, чтобы избежать этих проблем, например:

try
{
    try
    {
         //Reader-writer lock stuff
    }
    finally
    {
         //Release lock
    }
 }
 catch(Exception ex)
 {
    //Do something with exception
 }

Простое, последовательное, чистое решение - хорошая цель, но при условии, что вы не можете просто использовать lock(this){return mydateetc;}Вы могли бы пересмотреть подход; с дополнительной информацией я уверен, что переполнение стека может помочь;-)

Я лично использую оператор C# "using" настолько часто, насколько это возможно, но есть несколько конкретных вещей, которые я делаю вместе с ним, чтобы избежать потенциальных упомянутых проблем. Проиллюстрировать:

void doSomething()
{
    using (CustomResource aResource = new CustomResource())
    {
        using (CustomThingy aThingy = new CustomThingy(aResource))
        {
            doSomething(aThingy);
        }
    }
}

void doSomething(CustomThingy theThingy)
{
    try
    {
        // play with theThingy, which might result in exceptions
    }
    catch (SomeException aException)
    {
        // resolve aException somehow
    }
}

Обратите внимание, что я разделяю оператор "using" на один метод и использование объекта (ов) на другой метод с помощью блока "try"/"catch". Я могу вложить несколько "использующих" операторов, подобных этим, для связанных объектов (иногда я углубляюсь в мой рабочий код на три-четыре).

В моем Dispose() методы для этих пользовательских IDisposable классы, я ловлю исключения (но НЕ ошибки) и регистрирую их (используя Log4net). Я никогда не сталкивался с ситуацией, когда любое из этих исключений могло бы повлиять на мою обработку. Потенциальным ошибкам, как обычно, разрешено распространяться вверх по стеку вызовов и обычно завершать обработку с соответствующим сообщением (ошибка и трассировка стека).

Если бы я как-то столкнулся с ситуацией, когда во время Dispose()Я бы перепроектировал для этой ситуации. Честно говоря, я сомневаюсь, что это когда-нибудь случится.

В то же время, возможности и возможности "использования" делают его одной из моих самых любимых функций в C#. Кстати, я работаю на Java, C# и Python в качестве своих основных языков, с множеством других, добавленных сюда и там, и "использование" - одна из моих самых любимых языковых возможностей, потому что это практичная, повседневная рабочая лошадка.,

СУХОЙ говорит: второе решение. Первое решение дублирует логику использования блокировки, а второе - нет.

Мне нравится 3-й вариант

private object _myDateTimeLock = new object();
private DateTime _myDateTime;

public DateTime MyDateTime{
  get{
    lock(_myDateTimeLock){return _myDateTime;}
  }
  set{
    lock(_myDateTimeLock){_myDateTime = value;}
  }
}

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

"Связка свойств" и блокировка на уровне получателя и установщика свойства выглядит неправильно. Ваша блокировка слишком мелкая. При наиболее типичном использовании объектов вам нужно убедиться, что вы приобрели блокировку для доступа к нескольким свойствам одновременно. Ваш конкретный случай может быть другим, но я в этом сомневаюсь.

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

Блоки Try/Catch, как правило, предназначены для обработки исключений, в то время как блоки используются для гарантии удаления объекта.

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

using (obj)
{
  try { }
  catch { }
}

так что вы можете неявно вызывать ваш интерфейс IDisposable, а также делать обработку исключений лаконичной.

Следующее создает методы расширения для класса ReaderWriterLockSlim, которые позволяют вам делать следующее:

var rwlock = new ReaderWriterLockSlim();
using (var l = rwlock.ReadLock())
{
     // read data
}
using (var l = rwlock.WriteLock())
{
    // write data
}

Вот код:

static class ReaderWriterLockExtensions() {
    /// <summary>
    /// Allows you to enter and exit a read lock with a using statement
    /// </summary>
    /// <param name="readerWriterLockSlim">The lock</param>
    /// <returns>A new object that will ExitReadLock on dispose</returns>
    public static OnDispose ReadLock(this ReaderWriterLockSlim readerWriterLockSlim)
    {
        // Enter the read lock
        readerWriterLockSlim.EnterReadLock();
        // Setup the ExitReadLock to be called at the end of the using block
        return new OnDispose(() => readerWriterLockSlim.ExitReadLock());
    }
    /// <summary>
    /// Allows you to enter and exit a write lock with a using statement
    /// </summary>
    /// <param name="readerWriterLockSlim">The lock</param>
    /// <returns>A new object that will ExitWriteLock on dispose</returns>
    public static OnDispose WriteLock(this ReaderWriterLockSlim rwlock)
    {
        // Enter the write lock
        rwlock.EnterWriteLock();
        // Setup the ExitWriteLock to be called at the end of the using block
        return new OnDispose(() => rwlock.ExitWriteLock());
    }
}

/// <summary>
/// Calls the finished action on dispose.  For use with a using statement.
/// </summary>
public class OnDispose : IDisposable
{
    Action _finished;

    public OnDispose(Action finished) 
    {
        _finished = finished;
    }

    public void Dispose()
    {
        _finished();
    }
}

Хотя я согласен со многими из вышеупомянутых комментариев, включая детализацию блокировки и сомнительную обработку исключений, этот вопрос является одним из подходов. Позвольте мне привести одну серьезную причину, по которой я предпочитаю использовать абстракцию try {} finally...

У меня есть модель, очень похожая на вашу, за одним исключением. Я определил базовый интерфейс ILock и в нем я предоставил один метод под названием Acquire(). Метод Acquire () возвратил объект IDisposable, и в результате это означает, что пока объект, с которым я имею дело, имеет тип ILock, его можно использовать для создания области блокировки. Почему это важно?

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

interface ILock {
    IDisposable Acquire();
}

class MonitorLock : ILock {
    IDisposable Acquire() { ... acquire the lock for real ... }
}

Мне нравится ваша модель, но я бы подумал скрыть механику блокировки от звонящего. FWIW, я измерил издержки использования метода по сравнению с try-finally, и накладные расходы на размещение одноразового объекта будут иметь 2-3% накладных расходов производительности.

Я думаю, что метод 2 будет лучше.

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

Я удивлен, что никто не предложил инкапсулировать try-finally в анонимные функции. Так же, как и метод создания и удаления классов с помощью оператора using, это удерживает блокировку в одном месте. Я сам предпочитаю это только потому, что предпочитаю читать слово "наконец", чем слово "Утилизировать", когда я думаю о снятии блокировки.

class StackOTest
{
    private delegate DateTime ReadLockMethod();
    private delegate void WriteLockMethod();

    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            return ReadLockedMethod(
                rwlMyLock_m,
                delegate () { return dtMyDateTime_m; }
            );
        }
        set
        {
            WriteLockedMethod(
                rwlMyLock_m,
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private static DateTime ReadLockedMethod(
        ReaderWriterLock rwl,
        ReadLockMethod method
    )
    {
        rwl.AcquireReaderLock(0);
        try
        {
            return method();
        }
        finally
        {
            rwl.ReleaseReaderLock();
        }
    }

    private static void WriteLockedMethod(
        ReaderWriterLock rwl,
        WriteLockMethod method
    )
    {
        rwl.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwl.ReleaseWriterLock();
        }
    }
}

SoftwareJedi, у меня нет учетной записи, поэтому я не могу редактировать свои ответы.

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

class StackOTest
{
    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            DateTime retval = default(DateTime);
            ReadLockedMethod(
                delegate () { retval = dtMyDateTime_m; }
            );
            return retval;
        }
        set
        {
            WriteLockedMethod(
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private void ReadLockedMethod(Action method)
    {
        rwlMyLock_m.AcquireReaderLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseReaderLock();
        }
    }

    private void WriteLockedMethod(Action method)
    {
        rwlMyLock_m.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseWriterLock();
        }
    }
}

Фактически, в вашем первом примере, чтобы сделать решения сопоставимыми, вы бы также реализовали там "IDisposable" и вызывали "Dispose" из блока "finally" вместо того, чтобы снимать блокировку напрямую. Тогда вы будете реализованы по принципу "яблоки к яблокам" (и MSIL) (MSIL будет одинаковым для обоих решений). Тем не менее, хорошая идея использовать "использование" из-за добавленной области видимости и потому, что Framework обеспечит правильное использование "IDisposable" (последнее менее выгодно, если вы внедряете "IDisposabe" самостоятельно).

Дурак я. Есть способ сделать это еще проще, сделав заблокированные методы частью каждого экземпляра (вместо статического, как в моем предыдущем посте). Теперь я действительно предпочитаю это, потому что нет необходимости передавать `rwlMyLock_m'другому классу или методу.

class StackOTest
{
    private delegate DateTime ReadLockMethod();
    private delegate void WriteLockMethod();

    static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
    private DateTime dtMyDateTime_m;
    public DateTime MyDateTime
    {
        get
        {
            return ReadLockedMethod(
                delegate () { return dtMyDateTime_m; }
            );
        }
        set
        {
            WriteLockedMethod(
                delegate () { dtMyDateTime_m = value; }
            );
        }
    }

    private DateTime ReadLockedMethod(ReadLockMethod method)
    {
        rwlMyLock_m.AcquireReaderLock(0);
        try
        {
            return method();
        }
        finally
        {
            rwlMyLock_m.ReleaseReaderLock();
        }
    }

    private void WriteLockedMethod(WriteLockMethod method)
    {
        rwlMyLock_m.AcquireWriterLock(0);
        try
        {
            method();
        }
        finally
        {
            rwlMyLock_m.ReleaseWriterLock();
        }
    }
}
Другие вопросы по тегам