Подтвердить что ты не робот

Каков правильный способ добавления безопасности потока к объекту 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?
    }
}
4b9b3361

Ответ 1

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

Есть два оговорки:

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

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

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

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

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

Ответ 2

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

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

Ответ 3

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

Примерно так:

private 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);
}

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

Если вы только что имели в виду удаленную проверку - мой пример выше подойдет.

ОБНОВЛЕНИЕ:, чтобы ответить на комментарий "Какая разница между этим и флагом volatile bool? Немного сбивает с толку наличие поля с именем someCount и разрешение на него только для значений 0 и 1"

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

private volatile bool _disposed;

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

        // disposal code here
    }
}

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

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

Ответ 4

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

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

Ответ 5

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).


Обновление. Исходя из комментариев относительно значения make isDisposed volatile, обратите внимание, что проблема "забора" довольно тривиальна, поскольку я использую метод CheckDisposed(). Это, по сути, инструмент устранения неполадок, позволяющий быстро поймать случай, когда код вызывает публичный метод объекта после того, как он уже был удален. Вызов CheckDisposed() в начале открытого метода никоим образом не гарантирует, что объект не будет удален в этом методе. Если я считаю, что это риск, присущий дизайну моего класса, в отличие от условия ошибки, с которым мне не удалось выполнить учет, тогда я использую вышеупомянутый метод isDisposed и соответствующую блокировку.

Ответ 6

Вам нужно заблокировать каждый доступ к ресурсу, который вы собираетесь использовать. Я также добавил шаблон 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
    }
}