Daniel Fortunov


 

 Daniel Fortunov's Adventures in Software Development » .NET Event invocation thread safety

 2 Comments- Add comment | Back to Software Development Blog Written on 23-Apr-2009 by asqui

The Problem Statement

In CLR Via C# Richter points out a few subtle points about event invocation in multi-threaded classes:

  • A delegate chain is immutable; a new chain is created to replace the first.
  • A delegate chain with zero subscribers is null.
  • That means (if your event is public) it may transition from null to non-null and vice versa, at any time.

The Naive Approach: Not thread safe

So consider this invocation code which raises an event:

public static event EventHandler<EventArgs> NonThreadSafeEvent;

public static void OnNonThreadSafeEvent(EventArgs e)
{
if (NonThreadSafeEvent != null)
{
// Event could still become null in this interim,
// after the check but before the invocation
NonThreadSafeEvent(null, e);
}
}

On a class that is being accessed by multiple threads, this could lead to a NullReferenceException, despite the well-intentioned null guard.

The Solution Space

There are a number of ways to overcome this problem and guarantee that multi-threaded classes will never be exposed to the risk of a NullReferenceException when attempting to invoke a delegate or event.

The Classic Solution: Take a copy

This is the solution that Richter proposes to achieve thread safety:

public static event EventHandler<EventArgs> ClassicNullCheckedEvent;

public static void OnClassicNullCheckedEvent(EventArgs e)
{
EventHandler<EventArgs> localCopy = ClassicNullCheckedEvent;
if (localCopy != null)
{
// Nobody can change our local copy so we're sure it's not null
localCopy(null, e);
}
}

New-Age Solution: Pre-Initialise

I like to call this the Juval Löwy solution, because he proposes it in one of his books:

“You can ensure that the internal invocation list always has at least one member by initializing it with a do-nothing anonymous method. Because no external party can have a reference to the anonymous method, no external party can remove the method, so the delegate will never be null
   
— Programming .NET Components, 2nd Edition, by Juval Löwy

public static event EventHandler<EventArgs> PreInitializedEvent = delegate { };

public static void OnPreInitializedEvent(EventArgs e)
{
// No check required - event will never be null because
// we have subscribed an empty anonymous delegate which
// can never be unsubscribed. (But causes some overhead.)
PreInitializedEvent(null, e);
}

Of course then he immediately says that “initializing all delegates this way is impractical” yet without explaining why it is impractical. Seems fine to me! Certainly more practical than remembering to copy-and-check-for-null every time you want to raise an event.

Performance Implications

As always, there are some subtle performance implications to each approach (particularly the last one!)

Executing 50000000 iterations . . .
OnNonThreadSafeEvent took: 432ms OnClassicNullCheckedEvent took: 490ms OnPreInitializedEvent took: 614ms Subscribing an empty delegate to each event . . . Executing 50000000 iterations . . . OnNonThreadSafeEvent took: 674ms OnClassicNullCheckedEvent took: 674ms OnPreInitializedEvent took: 2041ms Subscribing another empty delegate to each event . . . Executing 50000000 iterations . . . OnNonThreadSafeEvent took: 2011ms OnClassicNullCheckedEvent took: 2061ms OnPreInitializedEvent took: 2246ms Done

Though you probably needn’t worry about these until your performance testing turns up a bottleneck on event invocation. i.e. probably never. (Note that the test run is for 50 million iterations.)

(Code samples from this post are available as a VS2008 Solution.)

Send to a friend

Comments

Leave a Comment









Loading …
  • Server: web1.webjam.com
  • Total queries:
  • Serialization time: 102ms
  • Execution time: 157ms
  • XSLT time: $$$XSLT$$$ms