向 IDisposable 对象添加线程安全性的正确方法是什么?
想象一下 IDisposable 接口的实现,它有一些公共方法。
如果该类型的实例在多个线程之间共享,并且其中一个线程可能会处置它,那么确保其他线程在处置后不会尝试使用该实例的最佳方法是什么?在大多数情况下,在对象被释放后,其方法必须知道它并抛出 ObjectDisposeException
或 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?
}
}
Imagine an implementation of the IDisposable
interface, that has some public methods.
If an instance of that type is shared between multiple threads and one of the threads may dispose it, what is the best way to ensure the other threads do not attempt to work with the instance after disposed? In most cases, after the object is disposed, its methods must be aware of it and throw the ObjectDisposedException
or maybe InvalidOperationException
or at least inform the calling code for doing something wrong. Do I need synchronization for every method - particularly around the check if it is disposed? Do all IDisposable
implementations with other public methods need to be thread-safe?
Here is an example:
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?
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
我倾向于使用整数而不是布尔值作为存储已处置状态的字段,因为这样您就可以使用线程安全的 Interlocked 类来测试 Dispose 是否已被调用。
像这样的事情:
这确保无论调用该方法多少次,处理代码都只被调用一次,并且是完全线程安全的。
然后每个方法都可以非常简单地使用调用此方法作为屏障检查:
关于同步每个方法 - 您是说简单的屏障检查不起作用 - 您想要停止可能已经在执行代码的其他线程在实例中。这是一个更复杂的问题。我不知道你的代码在做什么,但考虑一下你是否真的需要它 - 简单的屏障检查不行吗?
如果您只是指已处理的支票本身 - 我上面的例子就很好。
编辑:回答评论“这和 volatile bool 标志有什么区别?有一个名为 SomethingCount 的字段并允许它仅保存 0 和 1 值有点令人困惑”
Volatile 与确保读或写操作操作是原子且安全的。它不会使分配和检查值的过程线程安全。因此,例如,尽管有 volatile,以下内容也不是线程安全的:
这里的问题是,如果两个线程靠近在一起,第一个线程可能会检查 _dispose,读取 false,进入代码块并在将 _dispose 设置为 true 之前切换出去。然后第二个检查 _dispose,看到 false 并且也进入代码块。
使用 Interlocked 可确保分配和后续读取都是单个原子操作。
I tend to use an integer rather than a boolean as your field for storing the disposed status, because then you can use the thread-safe Interlocked class to test if Dispose has already been called.
Something like this:
This ensures that the disposal code is called only once not matter how many times the method is called, and is totally thread safe.
Then each method can quite simply use call this method as a barrier check:
With regard to synchronising every method - are you saying a simple barrier check won't do - that you want to stop other threads that might be already executing code in the instance. This is a more complex problem. I don't know what your code is doing, but consider if you really need that - will a simple barrier check not do?
If you just meant with regard to the disposed check itself - my example above is fine.
EDIT: to answer the comment "What's the difference between this and a volatile bool flag? It's slightly confusing to have a field named somethingCount and allow it to hold 0 and 1 values only"
Volatile is related to ensuring the read or write operation operation is atomic and safe. It doesn't make the process of assigning and checking a value thread safe. So, for instance, the following is not thread safe despite the volatile:
The problem here is that if two threads were close together, the first could check _disposed, read false, enter the code block and get switched out before setting _disposed to true. The second then checks _disposed, sees false and also enters the code block.
Using Interlocked ensures both the assignment and subsequent read are a single atomic operation.
您可以做的最简单的事情是将私有已处置变量标记为 易失性 并在方法的开头检查它。如果该对象已被释放,您可以抛出
ObjectDisposeException
。对此有两个注意事项:
如果该方法是事件处理程序,则不应抛出
ObjectDisposeException
。相反,如果可能的话,您应该优雅地退出该方法。原因是存在竞争条件,在您取消订阅事件后可以引发事件。 (请参阅这篇文章 Eric Lippert 了解更多信息。)当您正在执行某个类方法时,这并不能阻止您的类被释放。因此,如果您的类具有在处置后无法访问的实例成员,您将需要设置一些锁定行为以确保对这些资源的访问受到控制。
微软关于 IDisposable 的指南说你应该检查所有方法的 Dispose,但我个人认为没有必要。问题实际上是,如果您允许在类被释放后执行方法,是否会引发异常或导致意外的副作用。如果答案是肯定的,您需要做一些工作以确保不会发生这种情况。
关于所有 IDisposable 类是否都应该是线程安全的:否。一次性类的大多数用例都涉及它们仅由单个线程访问。
话虽这么说,您可能想调查为什么需要一次性类是线程安全的,因为它增加了很多额外的复杂性。可能有一个替代实现,使您不必担心一次性类中的线程安全问题。
The simplest thing you can do is mark the private disposed variable as
volatile
and inspect it at the beginning of your methods. You can then throw anObjectDisposedException
if the object has already been disposed.There are two caveats to this:
You shouldn't throw an
ObjectDisposedException
if the method is an event handler. Instead you should just gracefully exit from the method if that is possible. The reason being is that there exists a race condition where events can be raised after you unsubscribe from them. (See this article by Eric Lippert for more information.)This doesn't stop your class from being disposed while you are in the middle of executing one of your class methods. So if your class has instance members that can't be accessed after disposal, you're going to need to setup some locking behaviour to ensure access to these resources are controlled.
Microsoft's guidance around IDisposable says you should check for disposed on all methods, but I haven't personally found this necessary. The question really is, is something going to throw an exception or cause unintended side effects if you allow a method to execute after the class is disposed. If the answer is yes, you need to do some work to make sure that doesn't happen.
In terms of whether all IDisposable classes should be thread safe: No. Most of the use cases for disposable classes involve them only ever being accessed by a single thread.
That being said, you may want to investigate why you need your disposable class to be thread safe as it adds a lot of additional complexity. There may be an alternate implementation that allows you to not have to worry about thread safety issues in your disposable class.
大多数 Dispose 的 BCL 实现都不是线程安全的。这个想法是由 Dispose 的调用者来确保在 Dispose 之前没有其他人在使用该实例。换句话说,它将同步责任向上推。这是有道理的,否则现在所有其他消费者都需要处理对象在使用时被处置的边界情况。
也就是说,如果您想要一个线程安全的 Disposable 类,您可以在每个公共方法(包括 Dispose)周围创建一个锁,并在顶部检查 _dispose。如果您有长时间运行的方法并且您不想持有整个方法的锁,这可能会变得更加复杂。
Most BCL implementations of Dispose are not thread-safe. The idea is that it's up to the caller of Dispose to make sure nobody else is using the instance anymore before it is Disposed. In other words, it pushes the synchronization responsibility upwards. This makes sense, as otherwise now all your other consumers need to handle the boundary case where the object was Disposed while they were using it.
That said, if you want a thread-safe Disposable class, you can just create a lock around every public method (including Dispose) with a check for _disposed at the top. This may become more complicated if you have long-running methods where you don't want to hold the lock for the entire method.
我更喜欢在整数类型对象“dispose”或“state”变量上使用整数和 Interlocked.Exchange 或 Interlocked.CompareExchange ;如果 Interlocked.Exchange 或 Interlocked.CompareExchange 可以处理此类类型,我会使用 enum,但遗憾的是它们不能。
大多数关于 IDisposable 和终结器的讨论都没有提到的一点是,虽然对象的终结器在 IDisposable.Dispose() 正在进行时不应该运行,但类无法阻止其类型的对象被声明为死亡,然后复活了。可以肯定的是,如果外部代码允许这种情况发生,显然不能要求对象“正常工作”,但是 Dispose 和 Finalize 方法应该受到足够好的保护,以确保它们不会损坏任何 其他对象的状态,这通常需要对对象状态变量使用锁或
互锁
操作。I prefer to use integers and
Interlocked.Exchange
orInterlocked.CompareExchange
on an integer-type object "disposed" or "state" variable; I'd useenum
ifInterlocked.Exchange
orInterlocked.CompareExchange
could handle such types, but alas they cannot.One point which most discussions of IDisposable and finalizers fail to mention is that while an object's finalizer shouldn't run while IDisposable.Dispose() is in progress, there's no way for a class to prevent objects of its type from being declared dead and then resurrected. To be sure, if outside code allows that to happen there obviously can't be any requirement that the object "work normally", but the Dispose and finalize methods should be well-enough protected to ensure that they won't corrupt any other objects' state, which will in turn generally require using either locks or
Interlocked
operations on object state variables.您必须锁定对要处置的资源的所有访问权限。我还添加了我通常使用的 Dispose 模式。
You have to lock every access to the ressource you are going to dispose. I also added the Dispose pattern I normally use.
FWIW,您的示例代码与我和我的同事通常处理此问题的方式相匹配。我们通常在类上定义一个私有
CheckDispose
方法:然后我们在所有公共方法的顶部调用
CheckDispose()
方法。如果认为可能存在线程争用,而不是错误情况,我还将添加一个公共
IsDispose()
方法(类似于 Control.IsDispose)。更新:根据关于使
isDispose
易失性的值的评论,请注意,考虑到我如何使用CheckDispose()
方法,“栅栏”问题相当微不足道。它本质上是一个故障排除工具,用于快速捕获代码在对象已被释放后调用公共方法的情况。在公共方法开始处调用CheckDispose()
并不能保证该对象不会在该方法中被释放。如果我认为这是我的类设计中固有的风险,而不是我未能考虑到的错误条件,那么我将使用前面提到的IsDispose
方法以及适当的锁定。FWIW, your sample code matches how my co-workers and I typically deal with this issue. We generally define a private
CheckDisposed
method on the class:Then we call the
CheckDisposed()
method at the top of all public methods.If thread contention over disposal is considered likely, rather than an error condition, I will also add a public
IsDisposed()
method (Similar to Control.IsDisposed).Update: Based on the comments with respect to the value of making
isDisposed
volatile, note that the "fence" issue is rather trivial given how I use theCheckDisposed()
method. It is essentially a troubleshooting tool for quickly catching the case where code calls a public method on the object after it has already been disposed. CallingCheckDisposed()
at the start of a public method in no way guarantees that the object won't be disposed within that method. If I consider that to be a risk inherent in my class's design, as opposed to an error condition I failed to account for, then I use the aforementionedIsDisposed
method along with appropriate locking.