Using delegates and events in multithreaded apps

published: Tue, 16-Dec-2003   |   updated: Thu, 27-Oct-2005

If you declare a delegate in a class, you've become used to declaring and calling it like this, I would guess:

public delegate void DoSomethingDelegate(object sender, EventArgs e);

public class C {
  private DoSomethingDelegate doSomething;
  public DoSomethingDelegate DoSomething {
    get { return doSomething; }
    set { doSomething = value; }
  }
  public void OnDoSomething(EventArgs e) {
    if (doSomething != null) 
      doSomething(this, e);
  }
}

Well, unfortunately, this is the wrong way to call the delegate. Oh, don't get me wrong, it compiles and runs perfectly well in single-threaded applications, but in a multithreaded app, it hides a race condition.

Remember what a race condition is: the value of a variable changes between you reading it and using it, due to a change brought about by another thread. If we look at the method that invokes the delegate and analyze it line by line, statement by statement, we'll see the problem. Since there are only two lines, it won't be too difficult. The first line compares the doSomething variable to null. If it non-null, the second line is executed: the delegate is invoked. Sounds OK, but suppose the Evil Thread From Hell (ETFH, there's at least one in every multithreaded app) takes a timeslice in between the two lines and clears the delegate property to null. When the original thread resumes, we're assuming that the delegate variable is non-null and try to invoke it: Bam, null-exception city.

Instead we should do this:

  public void OnDoSomething(EventArgs e) {
    DoSomethingDelegate tempDoSomething = doSomething;
    if (tempDoSomething != null) 
      tempDoSomething(this, e);
  }

Here we copy the delegate variable (C# assures us that this assignment is an atomic operation), and then use the copy. It doesn't matter if ETFH clears the original delegate variable, we'll invoke the delegate anyway.

This pattern has a subtlety that needs to be teased out. Presumably the ETFH is in control of the method being called by the delegate (that's why it's clearing the delegate in the first place). The avoidance of the race condition produces another situation: the delegate method may get called even after setting the delegate variable to null. To see how this works, consider the situation from the ETFH's viewpoint. The main thread makes a copy of the delegate variable in our corrected code. EFTH gets a timeslice. It clears the delegate property since it doesn't want the method to be invoked any more (maybe the resources it needs for the delegate method have gone away). It does some other work. The main thread gets a timeslice and invokes the delegate copy and lo, the method gets called even when it's not a delegate target any more.

The upshot of this is that you must code your delegate methods to be resilient: they may get called even when they are no longer delegate targets.

So, now you know how to write events to succeed in multithreaded apps as well. Make a copy of the event, before testing and firing it, and be aware that an event handler may still get called after it has been removed from the event chain.