MSDN documents the thread-safety of instances members of BCL types pretty well, but I have never really seen information indicating how the Dispose
method of IDisposable
types can be called.
Is the Dispose
method a) guaranteed to be thread-safe for all classes, b) never guaranteed to be thread-safe, c) guaranteed to be thread-safe for some classes (if so, where is this specifically documented)?
Finally, if the Dispose
method is guaranteed to be thread-safe, does that mean I have to put a lock around each instance method in the class that uses disposable resources?
Side point: I'm aware that finalizers for types ought to be thread-safe due to the way garbage collection works in .NET (quite aggressively), and they may potentially call the Dispose
method. However, let's leave this issue aside for the point here.
The issue of thread-safety and Dispose is somewhat tricky. Since in many cases the only thing that any thread may legitimately do with an object once any other thread has started to dispose it is attempt to Dispose it itself, it would at first blush seem like the only thing necessary to ensure thread safety would be to use Interlocked.Exchange on a 'disposed' flag to ensure that one thread's Dispose attempt happens and the other is silently ignored. Indeed, that's a good starting point, and I think it should have been part of the standard Dispose pattern (the CompareExchange should have been done in the sealed base-class wrapper method, to avoid the need for every derived class to use its own private disposed flag). Unfortunately, if one considers what Dispose actually does, things are much more complicated.
The real purpose of Dispose is not to do something to the object being disposed, but rather to clean up other entities to which that object holds references. These entities may be managed objects, system objects, or something else entirely; they may not even be on the same computer as the object being disposed. For Dispose to be thread-safe, those other entities would to allow Dispose to clean them up at the same time as other threads might be doing other things with them. Some objects can handle such usage; others cannot.
One particular vexing example: Objects are allowed to have events with RemoveHandler methods that are not thread-safe. Consequently, any Dispose method which cleans up event handlers should only be called from the same thread as the one in which the subscriptions were created.
The page on MSDN here never actually explictly states that Dispose methods are not threadsafe, but to my reading, their code implies that no, they are not threadsafe and you need to account for that if needed.
Specifically the comments in the example code:
// 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;
}
}
}
I am fairly certain that unless otherwise noted, the Dispose()
method of any class will count as an 'instance member' for purposes of the documentation indicating thread safety or not.
So, if the documentation states that instance members are not thread safe, then neither will Dispose()
necessarily be, unless it is specifically noted as being different than the rest.