【问题标题】:Thread-Safety of Dispose methods?Dispose 方法的线程安全?
【发布时间】:2011-02-17 03:52:14
【问题描述】:

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

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

最后,如果保证Dispose 方法是线程安全的,那是否意味着我必须在类中使用一次性资源的每个实例方法上加一个锁?

旁白:我知道,由于垃圾收集在 .NET 中的工作方式(非常激进),类型的终结器应该是线程安全的,并且它们可能会调用 Dispose 方法。但是,让我们把这个问题放在一边。

【问题讨论】:

  • 谢谢,但这不是我要问的。此外,我在这里根本不关心终结器。
  • 再说一遍,您不应该明确地调用Dispose 并且不依赖Finalizer 线程来这样做吗?
  • @Chris O:当然;但推荐的做法是让终结器在任何情况下都处置非托管资源,作为后备。 MSDN 文章描述并演示了这种推荐的做法。
  • 这些天在 StackOverflow 上获得了投票,真是令人惊讶。可惜这样的问题很少受到关注。感谢所有回答/评论的人。但是。

标签: .net thread-safety dispose idisposable


【解决方案1】:

线程安全和 Dispose 的问题有些棘手。由于在许多情况下,一旦任何其他线程开始处置对象,任何线程唯一可以合法地对对象做的事情就是尝试处置它自己,乍一看,确保线程安全所需的唯一事情就是在 'disposed' 标志上使用 Interlocked.Exchange 以确保一个线程的 Dispose 尝试发生,而另一个线程被静默忽略。事实上,这是一个很好的起点,我认为它应该是标准 Dispose 模式的一部分(CompareExchange 应该在密封的基类包装方法中完成,以避免每个派生类都需要使用自己的私有处置标志)。不幸的是,如果考虑 Dispose 的实际作用,事情就会复杂得多。

Dispose 的真正目的不是对要被释放的对象做某事,而是清理该对象持有引用的其他实体。这些实体可能是托管对象、系统对象或其他东西;它们甚至可能与正在处理的对象不在同一台计算机上。为了使 Dispose 成为线程安全的,那些其他实体将允许 Dispose 在其他线程可能正在使用它们做其他事情的同时清理它们。一些对象可以处理这种用法;其他人不能。

一个特别令人烦恼的例子:对象被允许有带有非线程安全的 RemoveHandler 方法的事件。因此,任何清理事件处理程序的 Dispose 方法都只能从与创建订阅的线程相同的线程中调用。

【讨论】:

  • 感谢您的回复。这确实是一个相当复杂的问题。不过,我想我会接受 Interlocked.Increment 的建议。
【解决方案2】:

MSDN here 上的页面实际上从未明确声明 Dispose 方法不是线程安全的,但在我的阅读中,它们的代码暗示不,它们不是线程安全的,如果需要,您需要考虑这一点。

具体示例代码中的cmets:

// 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;   
        }
    }
}

【讨论】:

  • 是的,我注意到了这一点;这是一个有趣的例子。不幸的是,文档编写者并不总是与 BCL 程序员相提并论,而且无论如何也没有明确说明 BCL 类型的情况。不过,我怀疑您对普遍缺乏保证的看法是正确的。
  • 在相关的说明中,如果 Dispose 方法是线程安全的,那么对于处理 (即使用锁)?
  • @Noldorin:这个逻辑就是为什么我 99.999...% 在文档讨论线程安全时肯定“实例成员”包括Dispose()Dispose() 是一个实例成员,因此它应该具有与其余部分相同的线程安全属性。
  • @Andrew:是的,有道理。但是,如果在执行期间调用Dispose,我如何强制其他实例方法不会爆炸?查看 BCL 类型,即使是假定的“实例线程安全”类型似乎也没有对此采取预防措施。我我错过了一些微妙的东西?
  • 如果你的对象需要是线程安全的,那么它必须包含Dispose()。您基本上应该确保在其他方法完成时阻止对任何方法的调用,而不仅仅是 Dispose。反之亦然。
【解决方案3】:

我相当确定,除非另有说明,否则任何类的 Dispose() 方法都将被视为指示线程安全与否的文档的“实例成员”。

因此,如果文档声明实例成员不是线程安全的,那么 Dispose() 也不一定是线程安全的,除非特别指出它与其他成员不同。

【讨论】:

  • 我希望事情就这么简单!确实,Dispose 是一个实例方法,但我不喜欢缺少任何显式声明。说到这里,我可能只是用.NET Reflector 挖出一些例子......
  • 更具体地说,我正在考虑的一种情况是,从一个线程上的SocketStream 对象的读取操作中,我调用Dispose 在另一个线程上。这里会发生什么?读取是否会静默继续/进入损坏状态/抛出异常?
  • 如果类不保证实例方法的线程安全,你应该永远导致这种情况发生——你不知道会发生什么。
  • @Noldorin:我希望微软关于套接字的文档能够阐明允许哪些线程场景。最后我检查了一下,微软甚至没有说在一个线程上读取套接字同时在另一个线程上写入是合法的,即使在没有这样的承诺的情况下编写像 telnet 客户端这样的东西的唯一方法是忙于等待输入。实际上,在套接字上调用 Dispose 似乎是强制中止该套接字上任何挂起的阻塞操作的唯一安全方法。
  • @supercat:我也是。显然,根据 MSDN,Socket 的所有实例成员都是线程安全的。
猜你喜欢
  • 1970-01-01
  • 2012-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-13
  • 2021-11-06
  • 2013-12-16
  • 1970-01-01
相关资源
最近更新 更多