C# events examined
I recently ran into a threading bug involving adding handlers to events, and decided to take a deeper look at the compiler-generated event accessors.
Suppose you create a class like this one:
using System;
class C
{
public event EventHandler E;
public void AddHandler()
{
E += Handler; // thread-safe?
}
public void Handler(object sender, EventArgs e)
{
}
}
Is the statement in AddHandler thread-safe? I had heard that event accessors were thread-safe, but the truth is actually a bit more complicated. To find the answer, let's look at the code that actually gets generated for this class (or at least a C# rendering of the generated IL):
using System;
using System.Runtime.CompilerServices;
class C
{
private EventHandler E;
[MethodImpl(MethodImplOptions.Synchronized)]
public void add_E(EventHandler handler)
{
E = (EventHandler)Delegate.Combine(E, handler);
}
[MethodImpl(MethodImplOptions.Synchronized)]
public void remove_E(EventHandler handler)
{
E = (EventHandler)Delegate.Remove(E, handler);
}
public void AddHandler()
{
E = (EventHandler)Delegate.Combine(E, Handler);
}
public void Handler(object sender, EventArgs e)
{
}
}
Two things are interesting about this generated code: 1) The add and remove accessors used by event consumers outside the class are Synchronized to provide thread safety, as explained in section 10.8.1 of the C# Language Specification 3.0. 2) The AddHandler method uses the private delegate field rather than the add accessor, and therefore is not thread-safe. This second point is rather subtle, and is something I was only recently made aware of by an elusive threading bug in my code. (The C# language spec does mention this special case near the end of section 10.8.1: "Within the class X [which defines Ev], references to Ev are compiled to reference the hidden field __Ev instead".) AddHandler can be made thread-safe (with respect to itself and the generated accessors) by locking this
:
public void AddHandler()
{
lock (this)
{
E += Handler;
}
}
Incidentally, the "equivalent" code above is not entirely equivalent to the original code, in that it does not actually expose an event E that consumers can access using the += and -= operators. The closest you can get to equivalent source code is the following, which differs only from the compiler-generated code in that the compiler can name both the field and the event "E":
using System;
using System.Runtime.CompilerServices;
class C
{
private EventHandler e;
public event EventHandler E
{
[MethodImpl(MethodImplOptions.Synchronized)]
add // generates 'public void add_E(EventHandler)'
{
e = (EventHandler)Delegate.Combine(e, value);
}
[MethodImpl(MethodImplOptions.Synchronized)]
remove // generates 'public void remove_E(EventHandler)'
{
e = (EventHandler)Delegate.Remove(e, value);
}
}
public void AddHandler()
{
e += Handler; // generates e = (EventHandler)Delegate.Combine(e, Handler);
}
public void Handler(object sender, EventArgs e)
{
}
}
It's also interesting to note that if the AddHandler method was changed to use "E += Handler" instead of "e += Handler", the compiler would call add_E instead (since events with user-provided accessors are not "field-like events," as defined in the language spec).
Comments
- Anonymous
August 23, 2008
PingBack from http://hubsfunnywallpaper.cn/?p=2043