Written on 12-May-2009 by
asquiA day after my post on .NET Event invocation thread safety, a similar question came up on StackOverflow. About a week after, Eric Lippert came out with this blog post that discusses a related problem that is commonly rolled in to the confusion.
Here are my summary learnings:
- The problem of calling event handlers after they have been unsubscribed from the event is entirely separate from the null-protection problem.
Eric highlights that there are two separate problems here, with separate responsibilities - The event publisher must ensure they don’t cause a NullReferenceException by attempting to invoke an empty delegate chain after the last subscriber has unsubscribed from the event. (See my post on .NET Event invocation thread safety)
- The event subscriber must be able to deal with the fact that a handler may be called shortly after it has been unsubscribed from the event. (See next point.)
- “Event handlers are required to be robust in the face of being called even after the event has been unsubscribed”
Although not explicitly documented anywhere (yet) this conclusion is inevitable for multi-threaded code, particularly in light of the Jon Skeet’s thought experiment: “Now suppose that the invocation list for that delegate has 1000 entries. It's perfectly possible that the action at the start of the list will have executed before another thread unsubscribes a handler near the end of the list. However, that handler will still be executed because it'll be a new list. (Delegates are immutable.) As far as I can see this is unavoidable.” - Juval Löwy’s concern that compiler may optimise out the temporary copy of the delegate chain is unfounded
I avoided talking about this twist in the post on .NET Event invocation thread safety as it sounded a little too hypothetical. In his book, after describing the classic “Richter” take-a-copy-and-check-for-null pattern, Juval postulates that the compiler may optimise away the temporary copy for you, and undermine the pattern. Jon Skeet states that “The JIT isn't allowed to perform the optimization you're talking about in the first part, because of the condition. I know this was raised as a spectre a while ago, but it's not valid. (I checked it with either Joe Duffy or Vance Morrison a while ago; I can't remember which.)” so that’s a comfort.
The main thrust of the StackOverflow question is now:
Why is explicit-null-check the "standard pattern"? The alternative, assigning the empty delegate, requires only = delegate {} to be added to the event declaration, and this eliminates those little piles of stinky ceremony from every place where the event is raised. It would be easy to make sure that the empty delegate is cheap to instantiate. Or am I still missing something?
I still have some reservations about this, and suspect that the likes of Joe Duffy might have something to say about the performance impact of this. Although my performance analysis certainly suggests that the invocation-time overhead of this is, in real terms, negligible, I haven’t done any testing on the initialisation-time overheads. Still, the impact of this is likely to be negligible, and as Earwicker points out “Why let the ugly way be the recommended way? If we wanted premature optimisation instead of clarity, we'd be using assembler”
So, should we all be replacing the copy-and-null-check pattern with the initialise-with-empty-delegate pattern?