Thread-safe disposable objects

Most disposable objects are not thread-safe. After all, calling Dispose on one thread while other threads are accessing the object is bound to cause problems.

It is possible to make a disposable object entirely thread-safe, of course, but you’d need to take a lock inside your Dispose method and inside every other property or method call that uses disposable state to prevent that state from being disposed while you’re using it (or about to use it). Within that lock, you could safely check to see if your object is disposed and throw an ObjectDisposedException if it is.

To avoid the overhead of those locks, even our otherwise thread-safe objects have a disclaimer about the Dispose method: “This class is thread-safe except for Dispose, which is thread-compatible. Using an instance during or after its disposal results in unpredictable behavior.”

Whether a disposable object is entirely thread-safe or not, there are still obstacles when using disposable objects in multiple threads. Some disposable objects start background work as part of their job description; if the object is disposed while the background work is still running, that thread is likely to access disposed state and cause havoc. Similarly, clients of disposable objects may be doing work in background threads that use the object; if the client disposes the object while that background work is running, that thread is likely to access the object while it is being disposed, or after it is entirely disposed.

One solution to this problem is to catch ObjectDisposedException in your background work. If you get such an exception, you simply abandon the work on the assumption that it is no longer needed. Keep in mind that you’d have to make the disposable object entirely thread-safe for this to work, as described above; otherwise the object might be in the middle of a method call when it is disposed.

Our preferred solution is to cancel and wait for background work before disposing the object. (The mechanisms for canceling and waiting for background work are outside the scope of this post, though we may discuss it in a future post; needless to say, Thread.Abort is not a viable solution.)

In the case of the disposable object starting background work, it simply has to cancel and wait for background work as the first thing that it does in the Dispose method, before marking itself as being disposed or disposing any state.

In the case of the client with background work that is using the disposable object, the client should cancel and wait for that work before calling the Dispose method on that object.

There’s another scenario that’s somewhat common in our code base – what if the client that is running the background work isn’t the owner of the disposable object? How can the client cancel its background work before the object is disposed when it isn’t responsible for calling Dispose on that object?

We solve this problem by implementing a Disposing event on the disposable object. The Disposing event is raised by the Dispose method of the disposable object before any actual disposing takes place. The client can cancel and wait for background work inside its event handler for the Disposing event.

    public abstract class DisposableService : IDisposable
    {
        public event EventHandler Disposing;
        public void Dispose()
        {
            if (Interlocked.Exchange(ref m_nDisposing, 1) != 0)
                return;
            Disposing(this, EventArgs.Empty);
            Disposing = null;
            m_bDisposed = true;
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected DisposableService()
        {
            Disposing = delegate { OnDisposing(); };
        }
        protected void VerifyNotDisposed()
        {
            if (m_bDisposed)
                throw new ObjectDisposedException(GetType().Name);
        }
        protected virtual void Dispose(bool bDisposing)
        {
        }
        protected virtual void OnDisposing()
        {
        }
    #if DEBUG
        ~DisposableService()
        {
            Debug.Fail("Not disposed: " + GetType().Name);
        }
    #endif
        int m_nDisposing;
        volatile bool m_bDisposed;
    }

(In some cases where we have clients that depend on a disposable service, the client actually calls Dispose on itself in its handler for the Disposing event of that disposable service!)

All of this canceling and waiting inside Dispose methods and event handlers seems like a recipe for deadlock, but we haven’t found a better solution to the problem of using disposable objects from multiple threads, and it has been working pretty well for us so far.

(For more on this subject, see Joe Duffy’s recent post.)

Update: Inspired by Neil’s comment, I’ve added a bit more thread safety to Dispose. Now, even if multiple threads call Dispose at the “same time,” the Disposing event won’t be raised twice.

Posted by Ed Ball on March 05, 2008