想象一下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?
}
}
答案 0 :(得分:12)
Dispose的大多数BCL实现都不是线程安全的。这个想法是由Dispose的调用者来确保在Disposed之前没有其他人正在使用该实例。换句话说,它向上推动同步责任。这是有道理的,否则现在所有其他消费者都需要处理对象在使用时处置的边界情况。
也就是说,如果你想要一个线程安全的Disposable类,你可以在每个公共方法(包括Dispose)周围创建一个锁,并在顶部检查_disposed。如果你有长时间运行的方法,你不想持有整个方法的锁定,这可能会变得更加复杂。
答案 1 :(得分:10)
您可以做的最简单的事情是将私有处置变量标记为volatile
,并在方法的开头检查它。如果对象已被处理,则可以抛出ObjectDisposedException
。
有两点需要注意:
如果方法是事件处理程序,则不应抛出ObjectDisposedException
。相反,如果可能的话,你应该优雅地退出方法。原因是存在竞争条件,在您取消订阅后可以提高事件。 (有关详细信息,请参阅Eric Lippert的this article。)
这不会阻止您在执行某个类方法时处理您的类。因此,如果您的类具有在处置后无法访问的实例成员,则您将需要设置一些锁定行为以确保对这些资源的访问受到控制。
微软关于IDisposable的指导说你应该检查处理所有方法,但我个人认为没必要。问题实际上是,如果允许在处理类之后执行方法,则会引发异常或导致意外的副作用。如果答案是肯定的,那么你需要做一些工作以确保不会发生这种情况。
关于所有IDisposable类是否应该是线程安全的:否。一次性类的大多数用例涉及它们只能由单个线程访问。
话虽这么说,您可能想要研究为什么您需要您的一次性类是线程安全的,因为它增加了许多额外的复杂性。可能有一个替代实现,使您不必担心一次性类中的线程安全问题。
答案 2 :(得分:8)
我倾向于使用整数而不是布尔值作为存储处置状态的字段,因为这样您就可以使用线程安全的Interlocked类来测试是否已经调用了Dispose。
这样的事情:
private volatile int _disposeCount;
public void Dispose()
{
if (Interlocked.Increment(ref _disposeCount) == 1)
{
// disposal code here
}
}
这确保只调用一次处理代码,无论方法被调用多少次,并且完全是线程安全的。
然后每个方法都可以简单地使用call this方法作为屏障检查:
private void ThrowIfDisposed()
{
if (_disposeCount > 0) throw new ObjectDisposedException(GetType().Name);
}
关于同步每个方法 - 你是说一个简单的屏障检查不会 - 你想要阻止其他线程可能已经在实例中执行代码。这是一个更复杂的问题。我不知道你的代码在做什么,但考虑一下你是否真的需要它 - 一个简单的屏障检查不会吗?
如果你只关心处理的支票本身 - 我上面的例子很好。
编辑:回答评论“这和挥发性bool标志有什么区别?有一个名为somethingCount的字段并允许它只保留0和1值有点令人困惑”
易失性与确保读或写操作操作是原子的和安全的有关。它不会使分配和检查值线程的过程安全。因此,例如,尽管存在不稳定性,但以下内容并非线程安全:
private volatile bool _disposed;
public void Dispose()
{
if (!_disposed)
{
_disposed = true
// disposal code here
}
}
这里的问题是,如果两个线程靠近在一起,第一个可以检查_disposed,读取false,输入代码块并在将_disposed设置为true之前切换出来。第二个然后检查_disposed,看到false并进入代码块。
使用Interlocked可确保赋值和后续读取都是单个原子操作。
答案 3 :(得分:4)
我更喜欢在整数类型对象“dispos”或“state”变量上使用整数和Interlocked.Exchange
或Interlocked.CompareExchange
;如果enum
或Interlocked.Exchange
可以处理此类类型,我会使用Interlocked.CompareExchange
,但唉他们不能。{/ p>
IDisposable和终结器的大多数讨论都没有提到的一点是,虽然IDisposable.Dispose()正在进行时对象的终结器不应该运行,但是类没有办法阻止声明其类型的对象死了然后复活了。可以肯定的是,如果外部代码允许这种情况发生,那么显然不能要求对象“正常工作”,但Dispose和finalize方法应该得到足够的保护,以确保它们不会破坏任何其他对象的状态,通常需要对对象状态变量使用锁或Interlocked
操作。
答案 4 :(得分:1)
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
易变的价值的评论,请注意,鉴于我如何使用CheckDisposed()
方法,“围栏”问题相当简单。它本质上是一个故障排除工具,用于快速捕获代码在已经处置后调用对象上的公共方法的情况。在公共方法的开头调用CheckDisposed()
绝不保证该对象不会在该方法中处理。如果我认为这是我班级设计中固有的风险,而不是我没有考虑的错误条件,那么我使用前面提到的IsDisposed
方法以及适当的锁定。
答案 5 :(得分:0)
您必须锁定对要配置的资源的每次访问权限。我还添加了我通常使用的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
}
}