Click here to Skip to main content
Click here to Skip to main content

How to Safely Trigger Events the Easy Way

, 3 Mar 2010 CPOL
Rate this:
Please Sign up or sign in to vote.
A guide to the methods of triggering .NET events in a thread-safe way.

Contents

Introduction

Events in C# are simple things on the face of it. You create an event signature using a delegate type and mark it with the event keyword. External code can register to receive events. When specific things occur within your class, you can easily trigger the event, notifying all external listeners.

Easy, right?

Wrong.

If you are developing a multithreaded application, triggering events has a little catch. In this article, I'm going to discuss that catch and show you the standard ways of sidestepping the issue. Afterwards, I'm going to present a technique that will reduce the amount of repeated code and make everything a bit more readable.

I doubt I'm the first person to have suggested this idea, but I thought I would try to document the available options all in one place as a point of reference. I'd be interested to hear about other articles where code similar to that in this article is discussed.

I'm sure most experienced developers are aware of how to safely trigger events, so if you already know the basic bits, you might as well skip straight on down to the more interesting stuff.

The techniques and patterns discussed in this article are not applicable in every single case, but are useful in heavily multithreaded environments, or if you are writing libraries or frameworks that may be used on multiple threads.

How Not To Do It

OK, so I'm sure you've all seen code like this:

public class MyClass
{
    public event EventHandler MyEvent;

    public void DoSomething()
    {
        .
        .
        .
        if(MyEvent != null)
        {
            MyEvent(this, EventArgs.Empty);
        }
    }
}

Here, we have a test class which has a public event. When certain methods on the class are called, events are triggered, but there is a problem with this code. You have to check the MyEvent delegate for null, because it may be that you don't have any subscribers, but this null check introduces a race condition. Between the null check passing successfully and the event actually getting triggered, it is possible that one of your subscribers will have un-subscribed from the event on a different thread.

An analogy would be magazine subscription. If you un-subscribe for a magazine after it has already been posted by the provider, you are still going to receive the next issue. For exactly this reason, providers make you give them some notice when cancelling a subscription. In this code, we haven't given any notice, so it can just fail.

The Thread Safe Way

Because of this failure risk, some common patterns have evolved as a way of safely triggering events. The first and most common pattern is the one suggested by the Microsoft .NET Framework Design Guidelines.

public class MyClass
{
    public event EventHandler MyEvent;

    public void DoSomething()
    {
        .
        .
        .
        OnMyEvent();
    }

    public void OnMyEvent()
    {
        EventHandler localCopy = MyEvent;

        if(localCopy != null)
        {
            localCopy(this, EventArgs.Empty);
        }
    }
}

By taking a private copy of the delegate first, we are now safe to check it for null and trigger it knowing that it can't be changed between the null check and trigger. Any external callers are still free to un-subscribe from the public copy of the event.

Going back to the magazine subscription analogy, we are saying that if subscribers haven't cancelled their subscription by the point we copy the event, then it's too late, they will still receive one final magazine.

[For the observant among you, although EventHandler is a reference type, it inherits from MultiCastDelegate which, just like the String type, is immutable. This means that although to begin with, our "copy" will just be a reference to the same EventHandler object, when an external caller adds or removes a subscription to the event, the original object - our copy - doesn't change, but a new replacement EventHandler object is created with the newly updated invocation list.]

The second pattern is to pre-initialise your events with an empty subscriber:

public event EventHandler MyEvent = delegate { };

By doing this, you ensure that your event will never be null because you add one listener right from the start.

Many people prefer the first technique because in the second, you are creating an empty handler just to ensure something is never null. It's kind of the equivalent of initialising all of your strings with "" just to be sure they will never be null. On top of that, you have to be sure that you never clear the event, or the assumption is no longer valid, so rather than relying on checks at the time of the call, you are assuming that everything else within your class in your code is well behaved.

A third alternative is to simply handle (and silently ignore) the NullReferenceException that could arise from the event trigger.

public void OnMyEvent()
{
    try
    {
        localCopy(this, EventArgs.Empty);
    }
    catch(NullReferenceException)
    {
        \\ Do nothing.
    }
}

The potential problem with this final option is that you risk also catching and hiding NullReferenceExceptions that occurred within the event handler code. Although in well-behaved subscribers, they should be handling their own exceptions and not letting those exceptions filter up to the event owner, if you are writing frameworks or libraries, it's not always possible to rely on the behaviour of external code.

The technique that you use to ensure thread safety is down to you, but all of the three options described above present ways of eliminating the race condition in an event null check. Of course, if you are writing in a closed and purely single threaded environment, and you can be sure your objects won't be used in a cross-thread situation, it may be perfectly safe to avoid all of this all together.

Safe Trigger

If you do decide that you need fully thread-safe events, thanks to .NET 3.5's Extension Methods, we can write a little helper that does a safe null check for us. The advantage of doing this is that we reduce the amount of repeated code, and less repeated code means less chance of simple mistakes being made.

This is our SafeTrigger extension method:

public static void SafeTrigger<TEventArgs>
                (this EventHandler<TEventArgs> eventToTrigger,
                Object sender, TEventArgs eventArgs)
                where TEventArgs : EventArgs
{
    if (eventToTrigger != null)
    {
        eventToTrigger(sender, eventArgs);
    }
}

Let me explain. Here, we have an extension method that extends the EventHandler type with a new SafeTrigger(...) method. The safe trigger method is generic, to match the EventHandler and allow passing of a matching EventArgs class.

Within the method, you might expect to begin by copying the eventToTrigger as we did in the previous examples, but in actual fact, the copy is no longer necessary. Because we are passing the event reference in to a method, the reference is already being duplicated as part of the method call. This means we are just left to perform the null check and trigger the event. Now you can safely trigger your events in one simple line.

MyEvent.SafeTrigger(this, new MyEventArgs());

Conveniently, we don't actually have to specify the generic type, because C# can automatically infer what type we require from the type of the event the extension is acting on. Additionally, because the SafeTrigger method is an extension method, we don't have to be concerned about MyEvent being null before we call it.

We can take this convenience one step further by including the ability to return values from the EventArgs object.

public static TReturnType SafeTrigger<TEventArgs, TReturnType>
                (this EventHandler<TEventArgs> eventToTrigger, Object sender,
                TEventArgs eventArgs, 
                Func<TEventArgs, TReturnType> retrieveDataFunction)
                where TEventArgs : EventArgs
{
    if(retrieveDataFunction == null)
    {
        throw new ArgumentNullException("retrieveDataFunction");
    }

    if (eventToTrigger != null)
    {
        eventToTrigger(sender, eventArgs);
        TReturnType returnData = retrieveDataFunction(eventArgs);
        return returnData;
    }
    else
    {
        return default(TReturnType);
    }
}

This time, we are providing the option to pass a function which is used to extract data from the event arguments after the event has been triggered and return it, making it really simple to extract some data.

int returnedData = MyEvent.SafeTrigger(this, 
                     new MyEventArgs(), e => e.MyPropertyToReturn);

In this example, we pass a lambda for the function which describes the property (or properties) we want extracted from the EventArgs object.

[The downloads at the top of this article include source code for some further matching overloads of the SafeTrigger method to handle the non-generic form of the EventHandler class, and to make the EventArgs parameter optional - in which case, the default EventArgs.Empty or new TEventArgs() will be used.]

A Note on Inheritance

.NET Framework design guidelines state that for non-sealed classes, you should use a protected virtual On[EventName] method to trigger events. This is because you should provide sub classes as a way of hooking in on an event other than actually subscribing to the event. As such, the recommended pattern for use of the SafeTrigger extension would be as follows:

public void OnMyEvent(int inData)
{
    OnMyEvent.SafeTrigger(this, new MyEventArgs(inData));
}

Or:

public int OnMyEvent(int inData)
{
    return OnMyEvent.SafeTrigger(this, 
               new MyEventArgs(inData), e => e.OutData);
}

Conclusion

In this article, I started with an overview of why a simple null check was not a fully thread safe way of preparing to trigger an event. We've discussed the standard patterns available when safe alternatives are required, and I've ended with a demonstration of a way of encapsulating the Microsoft recommended pattern into a single method call.

By encapsulating common code in this way, we reduce the chance of mistakes being made when duplicating the code many times.

This kind of thread safety isn't necessary in every situation, but if you are writing multithreaded applications or frameworks and require strong thread-safety, the techniques discussed here are invaluable.

Further Reading

There is plenty of reading material online if you need more information on the nature of events and delegates in C#.

History

  • 27 Feb. 2010 - Initial revision.
  • 2 Mar. 2010 - Updated with a reference to MS guidelines, and added an exception handling pattern.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Simon P Stevens
Software Developer
United Kingdom United Kingdom
I discovered programming aged 11 with my school's BBC micro, and a book titled "Write your own space action games". (Their ideas of "space action" games were very different to mine. My ideas didn't include a bunch of * signs controlled via a text based menu)
 
I got hooked on VB for a while (mainly because I discovered I could replace the * signs with .bmp files) until someone pointed out the wonderful world of objects and Java. I also went thought a c++ phase.
 
I've now been a professional developer for 5 years.
 
My current language of choice is C#. I spend my free time playing with XNA and microcontrollers.
Follow on   Twitter

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberterrybozzio26-May-13 6:18 
GeneralMy vote of 5 Pinmembersucram9-Mar-12 2:30 
GeneralMy vote of 2 PinmemberJoe Sonderegger8-Mar-10 23:05 
GeneralRe: My vote of 2 PinmemberSimon P Stevens9-Mar-10 3:35 
GeneralA more generalized technique PinmvpPIEBALDconsult4-Mar-10 17:34 
GeneralRe: A more generalized technique PinmemberSimon P Stevens4-Mar-10 21:40 
GeneralMore options... and a "possible" problem. PinmemberPaulo Zemek4-Mar-10 6:14 
GeneralRe: More options... and a "possible" problem. PinmemberSimon P Stevens4-Mar-10 10:05 
GeneralRe: More options... and a "possible" problem. PinmemberPaulo Zemek4-Mar-10 10:37 
GeneralRe: More options... and a "possible" problem. PinmemberSimon P Stevens4-Mar-10 11:35 
GeneralRe: More options... and a "possible" problem. PinmemberPaulo Zemek5-Mar-10 3:40 
GeneralRe: More options... and a "possible" problem. PinmvpPIEBALDconsult4-Mar-10 10:56 
GeneralRe: More options... and a "possible" problem. PinmemberSimon P Stevens4-Mar-10 11:41 
GeneralRe: More options... and a "possible" problem. PinmvpPIEBALDconsult4-Mar-10 15:15 
GeneralThis is the kind of article I like PinmemberGary Wheeler2-Mar-10 0:18 
GeneralRe: This is the kind of article I like PinmemberSimon P Stevens2-Mar-10 1:11 
GeneralThoughts PinmvpPIEBALDconsult28-Feb-10 4:00 
GeneralRe: Thoughts PinmemberSimon P Stevens28-Feb-10 4:23 
GeneralRe: Thoughts PinmvpPIEBALDconsult28-Feb-10 17:55 
GeneralRe: Thoughts PinmemberSimon P Stevens3-Mar-10 10:57 
GeneralRe: Thoughts PinmvpPIEBALDconsult3-Mar-10 15:51 
GeneralRe: Thoughts PinmemberSimon P Stevens3-Mar-10 22:40 
GeneralRe: Thoughts PinmvpPIEBALDconsult4-Mar-10 4:31 
GeneralRe: Thoughts PinmemberSimon P Stevens4-Mar-10 4:47 
GeneralRe: Thoughts PinmvpPIEBALDconsult4-Mar-10 5:30 
GeneralRe: Thoughts PinmemberSimon P Stevens4-Mar-10 5:59 
GeneralYour have traded one race with another one PinmemberAlois Kraus28-Feb-10 0:39 
GeneralRe: Your have traded one race with another one [modified] PinmemberSimon P Stevens28-Feb-10 1:09 
GeneralRe: Your have traded one race with another one PinmemberAlois Kraus28-Feb-10 4:08 
GeneralRe: Your have traded one race with another one PinmemberDaniel Grunwald28-Feb-10 3:42 
GeneralRe: Your have traded one race with another one Pinmembersupercat91-Mar-10 5:45 
GeneralRe: Your have traded one race with another one PinmemberSimon P Stevens4-Mar-10 10:09 
GeneralInformative PinmemberSom Shekhar27-Feb-10 17:47 
GeneralRe: Informative PinmemberSimon P Stevens27-Feb-10 23:30 
GeneralHope it was helpful - Comments or suggestions welcome PinmemberSimon P Stevens27-Feb-10 13:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.141022.2 | Last Updated 3 Mar 2010
Article Copyright 2010 by Simon P Stevens
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid