Partager via


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