« Events and Threads (Part 2) | Main | Implementing Clone »

May 29, 2008

Events and Threads (Part 3)

We've discussed reasonable mechanisms for subscribing to events and for raising events, but we skirted the issue of "thread-safe" events until now.

What is a thread-safe event? A good definition would be "an event that may be subscribed, unsubscribed, and/or raised simultaneously on arbitrary threads." In that case, what must we do to create a thread-safe event?

Certainly it must be true that if you add an event handler, it is added, and if you remove an event handler, it is removed. As discussed earlier, the default implementation of the add and remove methods accomplishes this by locking the object, but I'd recommend using your own lock:

public event EventHandler Click

{

    add

    {

        lock (m_lockClick)

            m_click += value;

    }

    remove

    {

        lock (m_lockClick)

            m_click -= value;

    }

}

 

EventHandler m_click;

object m_lockClick = new object();

It is also certain that a thread-safe event must not throw a null reference exception when raising the event. The problem is that another thread could remove the last event handler at any moment, which sets the event delegate to null. In the following naïve implementation, Click could become null after the check but before the call:

private void RaiseClick()

{

    if (m_click != null)

        m_click(this, EventArgs.Empty);

}

The most common solution is to make a copy of the event delegate before calling it:

private void RaiseClick()

{

    EventHandler handler = m_click;

    if (handler != null)

        handler(this, EventArgs.Empty);

}

However, I learned from Juval Lowy's book that aggressive compiler inlining could theoretically eliminate the copy, which would bring us back to the same problem. His solution is to write a non-inlined method that raises the event, something like this:

private void RaiseClick()

{

    RaiseEvent(m_click);

}

 

[MethodImpl(MethodImplOptions.NoInlining)]

private void RaiseEvent(EventHandler handler)

{

    if (handler != null)

        handler(this, EventArgs.Empty);

}

Another good solution is to add a do-nothing event handler; follow the link for an explanation of that approach.

Of course, the most "correct" solution is probably to use the lock that's already there:

private void RaiseClick()

{

    EventHandler handler;

    lock (m_lockClick)

        handler = m_click;

    if (handler != null)

        handler(this, EventArgs.Empty);

}

Perhaps the last solution helped you think of another aspect of thread-safe events that isn't discussed very often. A problem common to all of these solutions is that a subscriber's event handler may be called even after it has been unsubscribed!

I found this behavior very surprising when I was writing thread-safe objects with events. For example, the Dispose method of one object might unsubscribe from an event of another object, assuming that the event handler won't be called again; but, in fact, that event handler might actually be called after the object has been disposed, which can obviously cause problems.

If you want to guarantee that an event handler won't be called after it is unsubscribed, as well as guarantee that an event handler can't be unsubscribed until the event is done being raised, the most direct solution is to call the event handler from within the lock:

private void RaiseClick()

{

    lock (m_lockClick)

    {

        if (m_click != null)

            m_click(this, EventArgs.Empty);

    }

}

This is a bit hair-raising, of course, because you're calling arbitrary code from within a lock, which is a good recipe for deadlock. I don't have enough experience with this pattern to know how common a problem that might be.

One final note about thread-safe events – make sure that your clients understand that their event handler will be invoked on an arbitrary thread, so that they know to dispatch to their UI thread if necessary.

I wish I had more solid conclusions as regards thread-safe events, but I'm still working through these issues. Hopefully I've at least given you some things to think about when you're considering adding events to a thread-safe class – it might be easier to just avoid them altogether.

Posted by Ed Ball at May 29, 2008 2:46 PM

Trackback Pings

TrackBack URL for this entry:
http://ancientblogs.logos.com/mt-cgi/mt-tb.cgi/212