将线程安全性添加到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? } } 

您可以做的最简单的事情是将私有处置变量标记为volatile ,并在方法的开头检查它。 如果已经处置了对象,则可以抛出ObjectDisposedException

这有两点需要注意:

  1. 如果方法是事件处理程序,则不应抛出ObjectDisposedException 。 相反,如果可能的话,你应该优雅地退出方法。 原因是存在竞争条件,在您取消订阅后可以提高事件。 (有关更多信息,请参阅Eric Lippert 撰写的这篇文章 。)

  2. 当你正在执行一个类方法时,这并不会阻止你的类处理。 因此,如果您的类具有在处置后无法访问的实例成员,则您将需要设置一些锁定行为以确保控制对这些资源的访问。

微软关于IDisposable的指导说你应该检查处理所有方法,但我个人认为没必要。 问题实际上是,如果允许在处理类之后执行方法,则会抛出exception或导致意外的副作用。 如果答案是肯定的,那么你需要做一些工作以确保不会发生这种情况。

关于所有IDisposable类是否应该是线程安全的:否。一次性类的大多数用例涉及它们只能被单个线程访问。

话虽这么说,你可能想要研究为什么你需要你的一次性类是线程安全的,因为它增加了很多额外的复杂性。 可能有一个替代实现,使您不必担心一次性类中的线程安全问题。

Dispose的大多数BCL实现都不是线程安全的。 这个想法是由Dispose的调用者来确保在Disposed之前没有其他人正在使用该实例。 换句话说,它向上推动同步责任。 这是有道理的,否则现在所有其他消费者都需要处理对象在使用时处置的边界情况。

也就是说,如果你想要一个线程安全的Disposable类,你可以在每个公共方法(包括Dispose)周围创建一个锁,并在顶部检查_disposed。 如果你有长时间运行的方法,你不想持有整个方法的锁,这可能会变得更加复杂。

我倾向于使用整数而不是布尔值作为存储处置状态的字段,因为这样您就可以使用线程安全的Interlocked类来测试是否已经调用了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); } 

关于同步每个方法 – 你是说一个简单的屏障检查不会 – 你想要停止可能已经在实例中执行代码的其他线程。 这是一个更复杂的问题。 我不知道你的代码在做什么,但考虑一下你是否真的需要它 – 一个简单的屏障检查不会吗?

如果你只是关于处理的支票本身 – 我上面的例子很好。

编辑:回答评论“这和挥发性bool标志有什么区别?有一个名为somethingCount的字段并允许它只保留0和1值有点令人困惑”

易失性与确保读或写操作操作是primefaces的和安全的有关。 它不会使分配检查值线程的过程安全。 因此,例如,尽管存在不稳定因素,但以下内容并非线程安全:

 private volatile bool _disposed; public void Dispose() { if (!_disposed) { _disposed = true // disposal code here } } 

这里的问题是,如果两个线程靠近在一起,第一个可以检查_disposed,读取false,输入代码块并在将_disposed设置为true之前切换出来。 然后第二个检查_disposed,看到false并进入代码块。

使用Interlocked可确保赋值和后续读取都是单个primefaces操作。

我更喜欢在整数类型对象“dispos”或“state”变量上使用整数和Interlocked.ExchangeInterlocked.CompareExchange ; 如果Interlocked.ExchangeInterlocked.CompareExchange可以处理这些类型,我会使用enum ,但唉他们不能。

IDisposable和终结器的大多数讨论都没有提到的一点是,虽然IDisposable.Dispose()正在进行时对象的终结器不应该运行,但是类没有办法阻止其类型的对象被声明为死,然后复活。 可以肯定的是,如果外部代码允许发生,那么显然不能要求对象“正常工作”,但Dispose和finalize方法应该得到足够的保护,以确保它们不会破坏任何其他对象’state,反过来通常要求对对象状态变量使用锁或Interlocked操作。

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 volatile的值的isDisposed ,请注意,鉴于我如何使用CheckDisposed()方法,“fence”问题相当简单。 它本质上是一个故障排除工具,用于快速捕获代码在已经处置后调用对象上的公共方法的情况。 在公共方法的开头调用CheckDisposed()绝不保证该对象不会在该方法中处理。 如果我认为这是我class级设计中固有的风险,而不是我未能解释的错误条件,那么我使用前面提到的IsDisposed方法以及适当的锁定。

您必须锁定对要配置的资源的每次访问。 我还添加了我通常使用的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 } }