Dispose方法的线程安全性?

12

MSDN很好地记录了BCL类型的实例成员的线程安全性,但我从未真正看到过有关如何调用IDisposable类型的Dispose方法的信息。

Dispose方法是否a)对于所有类都保证是线程安全的,b)从不保证是线程安全的,c)对于某些类保证是线程安全的(如果是这样的话,具体在哪里记录了)?

最后,如果Dispose方法被保证是线程安全的,那么意味着我必须在使用可释放资源的类中的每个实例方法周围放置锁定吗?

顺便说一下:我知道由于.NET的垃圾收集方式(非常积极),类型的终结器应该是线程安全的,并且它们可能会潜在地调用Dispose方法。然而,我们在这里暂时不考虑这个问题。


也许这可以帮助:https://dev59.com/sEXRa4cB1Zd3GeqPq1r3。 - Chris O
谢谢,但那不是我所询问的。此外,在这里我并不真的关心终结器。 - Noldorin
关于你提到的旁边问题,难道你不应该显式地调用 Dispose 而不是依赖 Finalizer 线程来处理吗? - Chris O
@Chris O:当然可以;但是推荐的做法是让终结器在任何情况下都处理非托管资源,作为一种备选方案。MSDN文章描述并演示了这种推荐做法。 - Noldorin
现在StackOverflow上被点赞的一些废话真是让人惊讶。很遗憾像这样的问题得到的关注如此之少。不过还是要感谢所有回答/评论的人。 - Noldorin
4个回答

9
线程安全和Dispose的问题有些棘手。由于在许多情况下,一旦任何其他线程开始处理它,任何线程只能合法地使用一个对象来尝试处理它自己,因此乍一看似乎确保线程安全所需的唯一事情就是使用Interlocked.Exchange对“disposed”标志进行操作,以确保一个线程的Dispose尝试发生并且另一个线程被静默忽略。实际上,这是一个很好的起点,我认为它应该是标准Dispose模式的一部分(CompareExchange应该在密封的基类包装器方法中完成,以避免每个派生类都使用自己的私有disposed标志)。不幸的是,如果考虑Dispose实际上要做的事情,情况会变得更加复杂。
Dispose的真正目的不是对正在处理的对象做出某些事情,而是清理那些持有引用该对象的其他实体。这些实体可以是托管对象、系统对象或完全不同的东西;它们甚至可能不在与被处理对象相同的计算机上。为了使Dispose线程安全,这些其他实体必须允许Dispose同时清理它们,同时其他线程可能正在执行其他操作。有些对象可以处理这样的用法,而有些则不能。
一个特别棘手的例子:对象允许具有不是线程安全的RemoveHandler方法的事件。因此,任何清理事件处理程序的Dispose方法只应该从创建订阅的同一线程中调用。

谢谢您的回复。这确实是一个非常复杂的问题。不过,我想我会采纳您的Interlocked.Increment建议。 - Noldorin

2
这里的MSDN页面没有明确说明Dispose方法不是线程安全的,但是根据我的阅读,它们的代码暗示了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;   
        }
    }
}

另外,如果Dispose方法是线程安全的,那么所有使用可释放资源的方法(即执行if(_disposed)检查的方法)在处理释放方面也应该是线程安全的(即使用锁)? - Noldorin
1
@Noldorin:这个逻辑是我99.999...%确定“实例成员”在文档讨论线程安全时包括Dispose()Dispose() 是一个实例成员,因此它应该具有与其余部分相同的线程安全属性。 - Andrew Barber
@Andrew: 是的,很有道理。但如果在其他实例方法执行期间调用Dispose,该怎么保证它们不会出错呢?即使是那些被认为“实例线程安全”的BCL类型似乎也没有对此进行预防措施。我是不是漏看了什么微妙之处? - Noldorin
@Noldorin 除了 Dispose() 之外,哪个 BCL 类型的所有实例成员都是线程安全的? 据我所知,没有这样的类。 - Andrew Barber
@AndrewBarber:如果一个方法在I/O上阻塞,而且已经意识到它等待的事情永远不会发生,一种常见的范例是允许在另一个线程上执行Dispose;一旦任何一个线程执行了Dispose,所有未决或将来的I/O操作将立即抛出异常。请注意,如果不能由外部线程执行Dispose,那么就没有办法取消任何阻塞的I/O操作。 - supercat
显示剩余2条评论

2
我相信,除非特别说明,否则任何类的Dispose()方法都将被视为“实例成员”,用于文档中指示线程安全性或不安全性。
因此,如果文档说明实例成员不是线程安全的,那么Dispose()也可能不是线程安全的,除非它被特别说明与其他方法不同。

我真希望事情像这样简单!确实,Dispose是一个实例方法,但我不喜欢缺乏任何明确语句的情况。话虽如此,我可能会挖掘一些使用.NET Reflector的示例... - Noldorin
如果类在实例方法上没有保证线程安全,那么你绝对不应该强行让它变得线程安全 - 因为你无法预知会发生什么。 - Andrew Barber
@Noldorin:我希望微软有关套接字的文档能够澄清哪些线程方案是允许的。据我所知,微软甚至没有说在一个线程上读取套接字而在另一个线程上写入是合法的,尽管如果没有这样的承诺,编写类似于 Telnet 客户端的唯一方法就是忙等待输入。实际上,调用 Dispose 方法来释放套接字似乎是强制中止该套接字上任何挂起的阻塞操作的唯一安全方式。 - supercat
@Noldorin: 所以它们确实都是。不过SerialPort不是;将我对Socket的抱怨应用于SerialPort。 - supercat
@Noldorin和@supercat - 我想知道你们是否混淆了线程安全性和线程亲和性。 - Andrew Barber
显示剩余2条评论

0

Dispose只是一个方法,Dispose本身没有什么特别之处。它的特殊用法是使用using语句-这样你就不需要记得写下面的代码了:

try { 使用对象做一些操作 } finally { object.Dispose(); }

所以Dispose不是线程安全的-因为唯一调用它的线程应该是拥有资源的线程。

永远不要在多个线程上调用单个对象的Dispose方法。如果你有多个线程同时处理同一个对象的Dispose,那你手头上会变成一团乱麻。

不要在多个线程上处理同一个对象的Dispose。这不仅是一个反模式(谁拥有这个对象?),而且是设计上的错误。

如果你正在使用Interlocked.Exchange来同步Dispose操作,那你已经丢失了对象的所有者。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接