« April 2008 | Main | June 2008 »

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 2:46 PM | Comments (7) | TrackBack

May 23, 2008

Events and Threads (Part 2)

It's time to continue our discussion of events and threads. You'll note in the last post that I didn't say much about "thread safe" events, because it's not clear what that would mean, particularly as regards the raising of an event. You won't see much in this post about "thread safe" events, either, though I do hope to get to that eventually.

We've already talked about adding and removing an event handler, so it's only natural that we would now talk about raising the event. The most commonly discussed problem that we face when raising an event in C# is that the event delegate is null if there are no subscribers.

public event EventHandler Click;

 

private void RaiseClick()

{

    // throws NullReferenceException if no subscribers

    Click(this, EventArgs.Empty);

}

In fact, I touched on this subject back in March, where I noted that assigning a do-nothing event handler to the event delegate avoids that problem entirely, though it does add a bit of inefficiency.

public event EventHandler Click = delegate { };

 

private void RaiseClick()

{

    // never throws NullReferenceException

    Click(this, EventArgs.Empty);

}

If your class has thread affinity, you must only raise the event from the UI thread, so you can safely do a null check without worrying about another thread removing the last event handler between the check and the call.

public event EventHandler Click;

 

private void RaiseClick()

{

    VerifyAccess();

 

    if (Click != null)

        Click(this, EventArgs.Empty);

}

If your class is thread-compatible, it must be assumed that you only raise an event from the thread that is currently accessing your instance, so, again, you can safely do a null check without worrying about other threads.

public event EventHandler Click;

 

private void RaiseClick()

{

    if (Click != null)

        Click(this, EventArgs.Empty);

}

But what if you want to raise an event in response to background work on a worker thread? In the case of a thread-affined class, there is usually a way to submit work to the UI thread, allowing you to raise the event from the UI thread. In WPF, you can use the Dispatcher for the UI thread.

public event EventHandler Click;

 

private void RaiseClick()

{

    Dispatcher.Invoke(DispatcherPriority.Send, new SendOrPostCallback(

        delegate

        {

            if (Click != null)

                Click(this, EventArgs.Empty);

        }), null);

}

In Windows Forms or WPF, you can use the SynchronizationContext of the UI thread.

public event EventHandler Click;

 

private void RaiseClick()

{

    m_context.Send(

        delegate

        {

            if (Click != null)

                Click(this, EventArgs.Empty);

        }, null);

}

 

SynchronizationContext m_context = SynchronizationContext.Current;

Raising an event in response to background work on a worker thread for a thread-compatible class is more interesting, because subscribers to the event will be called on an arbitrary thread. Therefore, for all intents and purposes, the event must be thread-safe, because it could be subscribed or unsubscribed on one thread and raised on another thread at the same time.

Which means that it's time to talk about thread-safe events, but I think I'll save that discussion for a future post.

Posted by Ed Ball at 1:15 PM | Comments (0) | TrackBack

May 9, 2008

Events and Threads (Part 1)

Once upon a time, I mentioned that I'd like to blog about thread-safety as it relates to events, so I figured I'd better get moving on that.

There are so many issues with .NET events and threads that it's hard to know where to begin, but let's start with the adding and removing of event handlers.

Unless documentation specifies otherwise, one must assume that adding and removing an event handler falls under the same thread safety requirements as any other method of the class. So, if the class has thread affinity (Windows Forms controls, WPF elements, etc.), assume that events can only be added and removed from the UI thread. If the class is thread-compatible (most non-UI classes in .NET), assume that events can be added and removed from any thread, but no two threads can add or remove events (or call any other method, for that matter) at the same time.

When authoring an event, if you allow C# to implement the add and remove methods (by not including your own), the default implementation attempts to be thread-safe by locking "this" before adding or removing the handler from the event delegate. In other words, these two events are implemented the same way:

public event EventHandler Event1;

 

public event EventHandler Event2

{

    add { lock (this) m_event2 += value; }

    remove { lock (this) m_event2 -= value; }

}

private EventHandler m_event2;

If your event has thread affinity or is thread-compatible, the lock is unnecessary overhead, so you're better off with a lock-free implementation:

public event EventHandler Event3

{

    add { m_event3 += value; }

    remove { m_event3 -= value; }

}

private EventHandler m_event3;

Better yet, if your event has thread affinity, make sure that the caller is on the UI thread.

public event EventHandler Event4

{

    add { VerifyAccess(); m_event4 += value; }

    remove { VerifyAccess(); m_event4 -= value; }

}

private EventHandler m_event4;

Furthermore, locking "this" is not recommended (see the MSDN documentation on the lock statement and on MethodImplOptions.Synchronized), so you might consider always implementing your own add and remove methods anyway.

While we're on the subject of adding and removing event handlers, if your class has more than a few events, consider using the EventHandlerList class to manage all of the event handlers, or manage the event handlers in a similar way with your own collection. This will save memory when many of the events have no subscribers. The EventHandlerList class is not thread-safe, which makes it most suitable for thread-affined and thread-compatible events.

There's obviously much more to discuss, not the least of which is a discussion of what it would mean for an event to be entirely thread-safe; hopefully part 2 won't be so long in coming!

Posted by Ed Ball at 10:25 AM | Comments (4) | TrackBack