12

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.

Mamta D
  • 6,310
  • 3
  • 27
  • 41
Noldorin
  • 144,213
  • 56
  • 264
  • 302
  • Maybe this could help: http://stackoverflow.com/questions/151000/finalizers-and-dispose. – Chris O Feb 17 '11 at 04:05
  • Thanks, but that's not quite what I'm asking about. Moreover, I don't really care about finalizers at all here. – Noldorin Feb 17 '11 at 04:07
  • Re your side point, shouldn't you be calling `Dispose` explicitly and not relying on the Finalizer thread to do so? – Chris O Feb 17 '11 at 04:07
  • @Chris O: Absolutely; but recommended practice is to let the finalizer dispose unmanaged resources in any case, as a fallback. The MSDN article describes and demonstrates this recommended practice. – Noldorin Feb 17 '11 at 04:10
  • Amazing what sort of bollocks is getting up-voted on StackOverflow these days. Shame that a question like this gets such little attention. Thanks to everyone who has answered/commented. however. – Noldorin Feb 17 '11 at 21:40

4 Answers4

9

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.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • Thanks for the response. It is indeed a pretty complex issue. I think I'll take you up on the `Interlocked.Increment` suggestion though. – Noldorin Feb 17 '11 at 23:53
2

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;   
        }
    }
}
David Hall
  • 32,624
  • 10
  • 90
  • 127
  • Yeah, I noticed this; it's an interesting example. Unfortunately the documentation writers aren't always on par with the BCL programmers, and it's not explicit about the case of BCL types anyway. I suspect you're right about the general lack of the guarantee though. – Noldorin Feb 17 '11 at 04:15
  • On a related note, if the Dispose method is thread-safe, shouldn't all methods that use disposable resources (i.e. do a `if (_disposed)` check) also be thread-safe with respect to disposing (i.e. use a lock)? – Noldorin Feb 17 '11 at 04:18
  • 1
    @Noldorin: That logic is why I'm 99.999...% sure that 'instance members' includes `Dispose()` when the docs are discussing thread safety. `Dispose()` *is an instance member*, so it should have the same thread-safety properties as the rest. – Andrew Barber Feb 17 '11 at 04:22
  • @Andrew: Right, makes sense. How might I enforce that other instance methods don't explode if `Dispose` is called during there execution, though"? Looking at BCL types even the supposable "instance thread-safe" ones don't seem to have precautions for this. Am I missing something subtle? – Noldorin Feb 17 '11 at 04:32
  • If your object needs to be thread-safe, then that must include `Dispose()`. You should basically assure that calls to any methods, not just Dispose, are blocked while others are completing. And the other way around. – Andrew Barber Feb 17 '11 at 04:55
  • @Noldorin What BCL type is thread-safe in all its instance members *except* `Dispose()`? None I'm aware of. – Andrew Barber Feb 19 '11 at 05:20
  • @AndrewBarber: If a method is blocking on I/O, and if one has become aware that the thing it's waiting for will never happen, a common paradigm is to allow `Dispose` on another thread; once any thread does a `Dispose`, all pending or future I/O operations will immediately throw an exception. Note that if `Dispose` could not be performed by an outside thread, there would be no way to cancel any blocking I/O. – supercat Feb 26 '13 at 00:59
2

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.

Andrew Barber
  • 39,603
  • 20
  • 94
  • 123
  • I do hope things are as simple as this! Indeed, `Dispose` is an instance method, but I'm not liking the lack of any explicit statement. Saying this, I may just dig out some examples with .NET Reflector... – Noldorin Feb 17 '11 at 04:16
  • To be more specific, one of the cases I'm considering is that *during* a read operation from a `Socket` or `Stream` object on one thread, I call `Dispose` on another thread. What happens here? Does the read silently continue/go into a corrupted state/throw an exception? – Noldorin Feb 17 '11 at 04:19
  • If the class does not guarantee thread safety on instance methods, you should *never* cause that to happen - you have no idea what will result. – Andrew Barber Feb 17 '11 at 04:20
  • @Noldorin: I wish Microsoft's documentation about sockets would clarify what threading scenarios are permissible. Last I checked, Microsoft didn't even say it was legal to read a socket on one thread while writing on another, even though the only way to write something like a telnet client without such a promise would be to busy-wait for input. In practice, calling Dispose on a socket seems to be the only safe way to force any pending blocking operations on that socket to be aborted. – supercat Feb 17 '11 at 18:11
  • @supercat: So do I. Apparently, all the instance members of `Socket` are thread-safe however, according to MSDN. – Noldorin Feb 17 '11 at 23:50
  • @Noldorin: So they are. SerialPort, however, isn't; apply my complaints about Socket to SerialPort. – supercat Feb 18 '11 at 00:12
  • @Noldorin & @supercat - I wonder if you guys are confusing Thread Safety with Thread Affinity. – Andrew Barber Feb 19 '11 at 05:21
0

Dispose is just a method, there is nothing special about Dispose. It's special use case is the using statement - so you don't have to remember to write

try { do something with object} finally { object.Dispose(); }

So Dispose is not thread-safe - why should it be as the only thread to call it should be the owning thread.

You should never call Dispose on a single object from multiple threads. While Dispose may not be thread-safe if you have multiple threads disposing of the SAME object then you have a spaghetti mess on your hands.

Do NOT dispose of a single object on multiple threads. That is not only an anti-pattern (who owns the object?) but a mistake in design.

If you are using Interlocked.Exchange to synchronize dispose then you've lost who the object owner is.