« Disposable value types | Main | Assigning to C# events »

March 5, 2008

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 at March 5, 2008 8:33 AM

Trackback Pings

TrackBack URL for this entry:
http://blog.logos.com/mt-cgi/mt-tb.cgi/189

Comments

Interesting stuff. To be even more thread safe, you'll want these two operations to somehow be atomic:

if (m_bDisposing)
return;

m_bDisposing = true;

Posted by: Neil Whitaker at March 7, 2008 11:45 PM

Great idea! I'll update the post.

Posted by: Ed Ball at March 10, 2008 11:09 AM

That is some interesting stuff, but I don't really understand why you write the following line:

Disposing = delegate { OnDisposing(); };

Is this a way of getting around writing the following syntax:

EventHandler threadSafeEventHandler = this.Disposing;
if (threadSafeEventHandler != null)
threadSafeEventHandler(this, EventArgs.Empty);

I can't find the reference article for that syntax, but the idea is that in a multithreaded environment, testing against the class exposed event handler is not thread safe (because a subscriber could unsubscribe between the test passing and the event being raised), so the event handler should first be copied to a local variable to guarantee thread safety.

Posted by: Josh at May 23, 2008 6:20 AM

Thanks for your comment! That line is basically a shortcut for calling OnDisposing when that event is raised, as well as avoiding the null check on the event delegate, as you suggest. See also http://code.logos.com/blog/2008/03/assigning_to_c_events.html .

Posted by: Ed Ball at May 23, 2008 9:44 AM

"...you'd need to take a lock inside your Dispose method and inside every other property or method call that uses disposable state...overhead of those locks..."

Some of the overhead of a lock can be avoided with an atomic reference count like the one below. Opposing threads set their intention to either access or shutdown the object being protected, and whoever loses either gives up or waits for the winner. Once the reference count reaches 0, no further threads may access the object being protected and shutdown may proceed safely.

public class AsyncUsage
{
private bool shutting_down = false;
private bool shut_down = false;
private int count = 1;
private ManualResetEvent unused = null;

public AsyncUsage()
{
unused = new ManualResetEvent(false);
}

// to be called at the beginning of some
// asynchronous callback; returns true
// if usage is acquired
public bool Acquire()
{
// attempt to reserve usage
Interlocked.Increment(ref count);

// check if further asynchronous
// operations are prohibited; this
// must occur _after_ the increment
bool acquired = !shutting_down;
if(!acquired)
{
Release();
}

return acquired;
}

// to be called at the end of some
// asynchronous callback
public void Release()
{
int new_count =
Interlocked.Decrement(ref count);
if(new_count == 0 && !shut_down)
{
shut_down = true;
unused.Set();
}
}

// to be called before any resources
// used by asynchronous callback(s) are
// disposed
public void FinalWaitRelease()
{
if(!shutting_down)
{
// prevent any new asynchronous
// operations from beginning;
// this must occur _before_ the
// Release
shutting_down = true;

// wait for all current
// asynchronous operations to
// finish
Release();
unused.WaitOne();

unused.Close();
unused = null;
}
}
}

Posted by: Matthew at June 8, 2009 8:20 PM

'not sure if my previous post will ever make it through. If it was of any interest, I just wanted to mention that there was a race condition in the AsyncUsage class:

if(new_count == 0 && !shut_down)
{
shut_down = true;
unused.Set();
}


that should be fixed in these two locations:

private int shut_down = 0;

...

if( new_count == 0 &&
Interlocked.Exchange(ref shut_down, 1) ==
0)
{
unused.Set();
}

Posted by: Matthew at June 9, 2009 11:02 AM

Post a comment




(you may use HTML tags for style)