2012-01-19 3 views
20

Представьте себе реализацию интерфейса IDisposable, который имеет некоторые общедоступные методы.Каков правильный способ добавления безопасности потока к IDisposable?

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


Вот пример:

public class DummyDisposable : IDisposable 
{ 
    private bool _disposed = false; 

    public void Dispose() 
    { 
     _disposed = true; 
     // actual dispose logic 
    } 

    public void DoSomething() 
    { 
     // maybe synchronize around the if block? 
     if (_disposed) 
     { 
      throw new ObjectDisposedException("The current instance has been disposed!"); 
     } 

     // DoSomething logic 
    } 

    public void DoSomethingElse() 
    { 
     // Same sync logic as in DoSomething() again? 
    } 
} 
+0

Я думаю, что вы смешиваете Dispose против финализаторов. Документы достаточно хорошо объясняют желаемое поведение: http: //msdn.microsoft.com/de-de/library/system.idisposable.dispose.aspx – weismat

+2

нет, я нет, но так как вы упомянули - 'Finalize()' насколько я помню. Тем не менее, Finalize вызывается, когда нет живых ссылок на объект, поэтому безопасность потоков не должна рассматриваться тогда. В лучших практиках MS рекомендуется вызывать Dispose внутри Finalize или, по крайней мере, использовать почти ту же логику - см. [Link] (http://msdn.microsoft.com/en-us/library/fs2xkftw%28v=vs.100 % 29.aspx). В любом случае это может привести к тому, что код 'readize()' в любом случае будет вызывать код, защищенный потоками. –

+0

Я добавил пример реализации, чтобы уточнить мой вопрос –

ответ

7

Самое простое, что вы можете сделать, это пометить личное расположенную переменную как volatile и проверить его в начале ваших методов. Затем вы можете выбросить ObjectDisposedException, если объект уже был удален.

Есть два предостережения к этому:

  1. Вы не должны бросить ObjectDisposedException если метод является обработчиком события. Вместо этого вы должны просто изящно выйти из метода, если это возможно. Причина в том, что существует условие гонки, в котором события могут быть подняты после того, как вы отмените подписку на них. (См. this article от Eric Lippert за дополнительной информацией.)

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

Руководства Microsoft по поводу IDisposable говорят, что вы должны проверять на все методы, но я лично не нашел это необходимым. На самом деле вопрос заключается в том, что что-то может вызвать исключение или вызвать непреднамеренные побочные эффекты, если вы разрешите метод выполнять после того, как класс будет удален. Если ответ «да», вам нужно сделать некоторую работу, чтобы убедиться, что этого не произойдет.

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

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

+0

Я думаю, что было бы разумнее реализовать либо Singleton, либо получить доступ к объекту через прокси-сервер, если вы хотите, чтобы все было просто. – weismat

+7

Сделать его волатильным не помешает любой из условий гонки. И их здесь несколько. –

+0

@ Хенк Да, он разрешает только одно условие гонки при входе метода (получение устаревшего значения для выделенного частного поля). Как я уже указывал в № 2 выше, вам все равно придется иметь дело с тем фактом, что объект все равно может быть удален в любой момент выполнения метода. –

10

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

Если вы хотите создать однопоточный класс Disposable, вы можете просто создать блокировку для каждого открытого метода (в том числе Dispose) с проверкой на _disposed вверху.Это может усложниться, если у вас есть длительные методы, когда вы не хотите удерживать блокировку для всего метода.

+10

Да, лучшим решением для Dispose-from-multiple-thread является: ** Не делайте этого **. –

+0

@ Хенк, если честно, вы действительно никогда не должны видеть ObjectDisposedException, если у вас нет ошибок. Однако, учитывая, что это инструмент отладки, я думаю, что разумно, чтобы у него не было гарантированного потокобезопасного поведения (т. Е. Вы могли бы работать с другими типами исключений, если вы используете объект из нескольких потоков сразу же после его размещения. .) –

+0

@ DanBryant это не проблема утилизации в многопоточной среде, но выполнение вызова во время процесса утилизации. Мы должны бросить 'ObjectDisposedException' в этом случае –

1

FWIW, ваш пример кода соответствует тому, как мои коллеги и я, как правило, занимаемся этой проблемой. Как правило, мы определим частный CheckDisposed метод на классе:

private volatile bool isDisposed = false; // Set to true by Dispose 

private void CheckDisposed() 
{ 
    if (this.isDisposed) 
    { 
     throw new ObjectDisposedException("This instance has already been disposed."); 
    } 
} 

Затем мы вызываем метод CheckDisposed() в верхней части всех общедоступных методов.

Если вопрос о разрешении на выбросы считается вероятным, а не условие ошибки, я также добавлю общедоступный метод IsDisposed() (аналогично Control.IsDisposed).


Обновление: На основе комментариев в отношении стоимости изготовления isDisposed изменчивы, обратите внимание, что «забор» вопрос довольно тривиально учитывая то, как я использую метод CheckDisposed(). Это, по сути, инструмент устранения неполадок, позволяющий быстро поймать случай, когда код вызывает публичный метод объекта после того, как он уже был удален. Вызов CheckDisposed() в начале общедоступного метода никоим образом не гарантирует, что объект не будет удален в этом методе. Если я считаю, что это риск, присущий дизайну моего класса, в отличие от условия ошибки, которое мне не удалось учесть, я использую вышеупомянутый метод IsDisposed вместе с соответствующей блокировкой.

+2

Вы также должны убедиться, что isDisposed отмечен как изменчивый или вы открыты для условия гонки. –

+0

Отличная точка. Образец изменен, чтобы включить объявление isDisposed. – dgvid

+2

@ Дэн, я не думаю, что забор поможет здесь, так как переупорядочение команд не является проблемой; реальная проблема заключается в том, что Dispose может быть вызван в любой точке, находясь в теле метода. –

7

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

Что-то вроде этого:

private volatile int _disposeCount; 

public void Dispose() 
{ 
    if (Interlocked.Increment(ref _disposeCount) == 1) 
    { 
     // disposal code here 
    } 
} 

Это гарантирует, что код утилизации вызывается только один раз, не имеет значения, сколько раз вызывается метод и полностью поточно.

Тогда каждый метод вполне может просто использовать этот метод называют в качестве проверки барьера:

private void ThrowIfDisposed() 
{ 
    if (_disposeCount > 0) throw new ObjectDisposedException(GetType().Name); 
} 

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

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

EDIT: ответить комментарий

Летучими является «В чем разница между этим и флагом летучего BOOL Это немного запутанным, чтобы иметь поле с именем somethingCount и позволяет ему удерживать 0 и 1 значение только?» связанные с обеспечением работы чтения или записи, являются атомными и безопасными. Он не выполняет процесс назначения и проверки безопасности потока значений. Так, например, следующий не поточно несмотря на летучий:

private volatile bool _disposed; 

public void Dispose() 
{ 
    if (!_disposed) 
    { 
     _disposed = true 

     // disposal code here 
    } 
} 

Проблема здесь состоит в том, что если два потока были близки друг к другу, первый мог проверить _disposed, прочитать ложь, введите блок кода и получить переключился перед установкой _disposed в true.Второй затем проверяет _disposed, видит false и также входит в блок кода.

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

+1

В чем разница между этим и изменчивым флагом bool? Немного смущает наличие поля с именем 'somethingCount' и позволяет ему удерживать только 0 и 1 значения. – Groo

+2

@Groo - см. Править. Жаль, что для bool нет блокированных перегрузок, но я бы предпочел, чтобы этот поточно-безопасный подход был небезопасным. –

+0

Это имеет смысл, если несколько потоков пытаются одновременно уничтожить объект (что было бы довольно странно). Но он по-прежнему не гарантирует, что он не будет удален в середине другого метода, сразу после выхода из ThrowIfDisposed. Это будет более распространенный случай IMHO, если вы передадите свой объект нескольким потокам. – Groo

0

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

public class MyThreadSafeClass : IDisposable 
{ 
    private readonly object lockObj = new object(); 
    private MyRessource myRessource = new MyRessource(); 

    public void DoSomething() 
    { 
     Data data; 
     lock (lockObj) 
     { 
      if (myResource == null) throw new ObjectDisposedException(""); 
      data = myResource.GetData(); 
     } 
     // Do something with data 
    } 

    public void DoSomethingElse(Data data) 
    { 
     // Do something with data 
     lock (lockObj) 
     { 
      if (myRessource == null) throw new ObjectDisposedException(""); 
      myRessource.SetData(data); 
     } 
    } 

    ~MyThreadSafeClass() 
    { 
     Dispose(false); 
    } 
    public void Dispose() 
    { 
     Dispose(true); 
     GC.SuppressFinalize(this); 
    } 
    protected void Dispose(bool disposing) 
    { 
     if (disposing) 
     { 
      lock (lockObj) 
      { 
       if (myRessource != null) 
       { 
        myRessource.Dispose(); 
        myRessource = null; 
       } 
      } 
      //managed ressources 
     } 
     // unmanaged ressources 
    } 
} 
+1

вы можете объяснить, почему вы называете 'GC.SuppressFinalize (this)' в финализаторе - не слишком ли поздно для этого, если GC уже удаляет экземпляр? Возможно, позвоните после успешного удаления, чтобы облегчить GC? –

+0

Вы правы. Это был надзор и должен быть в Dispose(). –

2

Я предпочитаю использовать целые числа и Interlocked.Exchange или Interlocked.CompareExchange на объект целочисленного типа «расположенный» или «состояние» переменной; Я бы использовал enum, если Interlocked.Exchange или Interlocked.CompareExchange мог обрабатывать такие типы, но, увы, они не могут.

Один из вопросов, о которых большинство обсуждений IDisposable и финализаторов не упоминает, заключается в том, что, хотя финализатор объекта не должен запускаться во время IDisposable. Dispose() в данный момент не существует способа, чтобы класс не позволял объектам своего типа объявлен мертвым, а затем воскрешен. Разумеется, если внешний код позволяет это осуществить, очевидно, не может быть никаких требований к тому, чтобы объект «работал нормально», но методы «Утилизация» и «Доработка» должны быть достаточно защищены, чтобы гарантировать, что они не будут повреждать любые другие состояние объектов, которое, как правило, обычно требует использования либо замков, либо Interlocked операций с переменными состояния объекта.

Смежные вопросы