Потоковая безопасность методов утилизации?

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

Является ли метод Dispose а) гарантированно поточно-ориентированным для всех классов, б) никогда не гарантированно поточно-ориентированным, в) гарантированно поточно-ориентированным для некоторых классов (если да, то где это конкретно задокументировано)?

Наконец, если гарантировано, что метод Dispose будет потокобезопасным, означает ли это, что я должен установить блокировку для каждого метода экземпляра в классе, который использует одноразовые ресурсы?

Дополнительный момент: я знаю, что финализаторы для типов должны быть потокобезопасными из-за того, как сборка мусора работает в .NET (довольно агрессивно), и они потенциально могут вызывать метод Dispose. Однако оставим этот вопрос в стороне.


person Noldorin    schedule 17.02.2011    source источник
comment
Возможно, это может помочь: stackoverflow.com/questions/151000/finalizers-and-dispose.   -  person Chris O    schedule 17.02.2011
comment
Спасибо, но я не совсем об этом. Более того, меня здесь совсем не волнуют финализаторы.   -  person Noldorin    schedule 17.02.2011
comment
Что касается вашей стороны, разве вы не должны вызывать Dispose явно и не полагаться на поток Finalizer для этого?   -  person Chris O    schedule 17.02.2011
comment
@Chris O: Совершенно верно; но рекомендуемая практика - позволить финализатору избавляться от неуправляемых ресурсов в любом случае в качестве запасного варианта. Статья MSDN описывает и демонстрирует эту рекомендуемую практику.   -  person Noldorin    schedule 17.02.2011
comment
Удивительно, за какую чушь проголосовали в наши дни на StackOverflow. Жалко, что такому вопросу уделяется так мало внимания. Спасибо всем, кто ответил / прокомментировал. тем не мение.   -  person Noldorin    schedule 18.02.2011


Ответы (3)


Проблема безопасности потоков и Dispose несколько сложна. Поскольку во многих случаях единственное, что любой поток может законно делать с объектом после того, как какой-либо другой поток начал избавляться от него, это попытка его самого удаления, на первый взгляд может показаться, что единственное, что необходимо для обеспечения безопасности потока, - это используйте Interlocked.Exchange для флага disposed, чтобы гарантировать, что попытка Dispose одного потока произойдет, а другой будет молча проигнорирован. Действительно, это хорошая отправная точка, и я думаю, что она должна была быть частью стандартного шаблона Dispose (CompareExchange должен был быть выполнен в методе запечатанной оболочки базового класса, чтобы избежать необходимости для каждого производного класса использовать свой собственный частный класс. убранный флаг). К сожалению, если учесть, что на самом деле делает Dispose, все намного сложнее.

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

Один особенно неприятный пример: объектам разрешено иметь события с методами RemoveHandler, которые не являются потокобезопасными. Следовательно, любой метод Dispose, который очищает обработчики событий, должен вызываться только из того же потока, что и тот, в котором были созданы подписки.

person supercat    schedule 17.02.2011
comment
Спасибо за ответ. Это действительно довольно сложный вопрос. Думаю, я подойду к вам на предложение Interlocked.Increment. - person Noldorin; 18.02.2011

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

В частности, комментарии в примере кода:

// This class shows how to use a disposable resource.
// The resource is first initialized and passed to
// the constructor, but it could also be
// initialized in the constructor.
// The lifetime of the resource does not 
// exceed the lifetime of this instance.
// This type does not need a finalizer because it does not
// directly create a native resource like a file handle
// or memory in the unmanaged heap.

public class DisposableResource : IDisposable
{

    private Stream _resource;  
    private bool _disposed;

    // The stream passed to the constructor 
    // must be readable and not null.
    public DisposableResource(Stream stream)
    {
        if (stream == null)
            throw new ArgumentNullException("Stream in null.");
        if (!stream.CanRead)
            throw new ArgumentException("Stream must be readable.");

        _resource = stream;

        _disposed = false;
    }

    // Demonstrates using the resource. 
    // It must not be already disposed.
    public void DoSomethingWithResource() {
        if (_disposed)
            throw new ObjectDisposedException("Resource was disposed.");

        // Show the number of bytes.
        int numBytes = (int) _resource.Length;
        Console.WriteLine("Number of bytes: {0}", numBytes.ToString());
    }


    public void Dispose() 
    {
        Dispose(true);

        // Use SupressFinalize in case a subclass
        // of this type implements a finalizer.
        GC.SuppressFinalize(this);      
    }

    protected virtual void Dispose(bool disposing)
    {
        // If you need thread safety, use a lock around these 
        // operations, as well as in your methods that use the resource.
        if (!_disposed)
        {
            if (disposing) {
                if (_resource != null)
                    _resource.Dispose();
                    Console.WriteLine("Object disposed.");
            }

            // Indicate that the instance has been disposed.
            _resource = null;
            _disposed = true;   
        }
    }
}
person David Hall    schedule 17.02.2011
comment
Да, я заметил это; это интересный пример. К сожалению, составители документации не всегда находятся на одном уровне с программистами BCL, и в любом случае это не совсем ясно о случае типов BCL. Я подозреваю, что вы правы насчет общего отсутствия гарантии. - person Noldorin; 17.02.2011
comment
В связи с этим, если метод Dispose является потокобезопасным, не должны ли все методы, использующие одноразовые ресурсы (т.е. выполнять проверку if (_disposed)), также быть потокобезопасными в отношении удаления (т.е. использовать блокировку)? - person Noldorin; 17.02.2011
comment
@Noldorin: Именно по этой логике я на 99,999 ...% уверен, что «члены экземпляра» включают Dispose(), когда документы обсуждают безопасность потоков. Dispose() является членом экземпляра, поэтому он должен иметь те же свойства безопасности потоков, что и остальные. - person Andrew Barber; 17.02.2011
comment
@ Андрей: Верно, в этом есть смысл. Как я могу обеспечить, чтобы другие методы экземпляра не взрывались, если Dispose вызывается во время их выполнения? Глядя на типы BCL, даже предполагаемые поточно-ориентированные экземпляры, похоже, не имеют для этого мер предосторожности. Я упустил что-то тонкое? - person Noldorin; 17.02.2011
comment
Если ваш объект должен быть потокобезопасным, он должен включать Dispose(). Вы должны в основном гарантировать, что вызовы любых методов, а не только Dispose, заблокированы, пока другие завершаются. И наоборот. - person Andrew Barber; 17.02.2011
comment
@Noldorin Какой тип BCL является потокобезопасным во всех членах экземпляра кроме Dispose()? Ни о чем мне известно. - person Andrew Barber; 19.02.2011
comment
@AndrewBarber: если метод блокирует ввод-вывод, и если кто-то узнал, что то, чего он ждет, никогда не произойдет, общая парадигма - разрешить Dispose в другом потоке; как только какой-либо поток выполнит Dispose, все ожидающие или будущие операции ввода-вывода немедленно вызовут исключение. Обратите внимание, что если Dispose не может быть выполнено внешним потоком, не будет возможности отменить любой блокирующий ввод-вывод. - person supercat; 26.02.2013

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

Итак, если в документации указано, что члены экземпляра не являются потокобезопасными, то это не обязательно Dispose(), если это специально не отмечено как отличное от остальных.

person Andrew Barber    schedule 17.02.2011
comment
Я очень надеюсь, что все так просто! Действительно, Dispose - это метод экземпляра, но мне не нравится отсутствие каких-либо явных утверждений. Сказав это, я могу просто откопать несколько примеров с .NET Reflector ... - person Noldorin; 17.02.2011
comment
Чтобы быть более конкретным, я рассматриваю один из случаев, когда во время операции чтения из объекта Socket или Stream в одном потоке я вызываю Dispose в другом потоке. Что здесь происходит? Продолжает ли чтение без уведомления / переходит в поврежденное состояние / вызывает исключение? - person Noldorin; 17.02.2011
comment
Если класс не гарантирует потокобезопасность в методах экземпляра, вы не должны никогда вызывать это - вы не знаете, к чему это приведет. - person Andrew Barber; 17.02.2011
comment
@Noldorin: Я бы хотел, чтобы документация Microsoft о сокетах прояснила, какие сценарии потоковой передачи допустимы. В последний раз, когда я проверял, Microsoft даже не сказала, что законно читать сокет в одном потоке во время записи в другом, хотя единственный способ написать что-то вроде telnet-клиента без такого обещания - это ожидание ввода. На практике вызов Dispose в сокете кажется единственным безопасным способом принудительного прерывания любых ожидающих блокирующих операций в этом сокете. - person supercat; 17.02.2011
comment
@supercat: Я тоже. Очевидно, согласно MSDN, все члены экземпляра Socket поточно-ориентированы. - person Noldorin; 18.02.2011
comment
@Noldorin: Так оно и есть. Однако SerialPort - нет; Применяю мои жалобы на Socket к SerialPort. - person supercat; 18.02.2011
comment
@Noldorin & @supercat - Не путаете ли вы, ребята, безопасность потоков с сходством потоков. - person Andrew Barber; 19.02.2011