다음을 통해 공유


Events get a little overhaul in C# 4, Part I: Locks

It’s been a long time since I’ve posted anything about the way the compiler generates field-like events, and I have some good news. We fixed them! Well, anyway, we changed them, and I believe that in C# 4, they are better than they used to be.

Read this old post to refresh yourself, if you care. The cause for alarm, through C# 3, was that when you declared events, the compiler made event accessors that took a lock on the “this” sync block, or worse. This meant that calls to the accessors were safe to make on multiple threads, but it is bad design that could cause deadlocks, and made it hard to program lock-free using events.

Let’s have a look at what changed. Suppose we have the following simple event declaration:

 class Control
{
    public event EventHandler OnClick;
}

In C# 3, we generated event handlers that looked something like this

 class Control
{
    private EventHandler __OnClick; // This is the backing field
    public event EventHandler OnClick
    {
        add { lock (this) { __OnClick = __OnClick + value; } }
        remove { lock (this) { __OnClick = __OnClick - value; } }
    }
}

That old post explains it in some detail, but the overall badness comes from the lock. You should never lock “this.” So, the question becomes, what can we do to improve the codegen here?

Well, we really have to get rid of that lock. There are a variety of options available to us. We can just delete the lock entirely and leave the delegate operations naked. Or we can replace it with a lock on something safe (say, a field we’d create just for this purpose in your class). Or we can introduce some other synchronization mechanism.

If we got rid of the lock, though, that would really be a problem for people who need this synchronization. After all, it was added for a reason, and surely people are relying on it. As for the second option, well, that would bloat your instances all by a reference, which you might or might not care about, but someone does. After all, the compiler is going to be doing this for everyone who uses field-like events.

So, we went with option three. Now, the compiler generates something like this:

 class Control
{
    private EventHandler __OnClick; // This is the backing field
    public event EventHandler OnClick
    {
        add { /* lock-free synchronization code that updates __OnClick */ }
        remove { /* lock-free synchronization code that updates __OnClick */ }
    }
}

Well, that’s not so instructive, but I don’t want to sidetrack you with the details of exactly what the code looks like (it’s a compare-and-swap). It’s interesting, but it’s not the point (go look at your classes with ildasm and you can see it; we’ll talk later). With this new code, in C# 4, your event accessors will have the following characteristics:

  1. The accessors do not use the Monitor class at all, and can therefore be used in an environment that is hostile to locks, such as SQLCLR.
  2. For each field-like event, all add and remove calls will be effectively serial, regardless of the threads the calls take place on. Adds and removes on separate events can happen simultaneously, but that’s no problem.
  3. This synchronization works for instance events and static events, and it works in reference types (classes) as well as value types (structs). This is an improvement over the previous model that did not support value types.

These are really good things. But there is some cost; unfortunately it’s not all Tootsie Rolls and unicorns. I’ll follow up with more about the downsides in the next few posts, as well as other compiler changes that mitigate them.

Comments

  • Anonymous
    March 04, 2010
    One interesting thing about this bit is that although the Microsoft spec insists locking on "this", the ECMA spec (still back in C# 2 land of course) says that the compiler can choose what to lock on... so it could have generated a synthetic field.
  • Anonymous
    March 04, 2010
    @"But there is some cost; unfortunately it’s not all Tootsie Rolls and unicorns"I wonder if it has anything to do with the fact that the generated add/remove code is pretty big. Why not calling a helper method from add/remove to do all the work?
  • Anonymous
    March 05, 2010
    Compare and Swap can be used to solve the consensus problem for any number of processes.  It seems like this, or some other universal object, would be perfect for the situation.  Would you comment on why locking was suggested in the standard, instead of some vague text like "shall be thread safe" which would have left more leeway in implementation?Locking does guarantee synchronization and I expect the standard authors knew locking would exist.  Also, locking around this or a field generated specifically for this event would be straight forward to implement.  Perhaps that has something to do with it.Just curious,Ed
  • Anonymous
    March 05, 2010
    Interesting... I guess you'll get into it more in Part 2, but is the new implementation more performant in a single threaded environment?  I'm thinking about environments like WPF where event handlers are constantly being hooked and unhooked for virtualized UI and how this might affect it.
  • Anonymous
    March 08, 2010
    The comment has been removed
  • Anonymous
    March 11, 2010
    Hi Chris, While it's harder to propagate a helper to reduce size to every runtime, you can generate this helper once per assembly.This wlll actually reduce the size compare to CLR v2 for almost all cases. Save on JIT time and code size.
  • Anonymous
    March 14, 2010
    Hi Chris,I was just wondering, is there any reason not to lock lock on the delegate field (__OnClick) in the first place ? It seems to me that it would have been the sensible thing to do... but I'm probably missing something here ;)
  • Anonymous
    March 16, 2010
    "I was just wondering, is there any reason not to lock lock on the delegate field (__OnClick) in the first place ?"I'm not Chris, but I'll suggest that you're missing something there.  :)Consider what the value of that field is the first time any code subscribes to the event. How should the add accessor lock on null?Consider also the problem of having the field updated while one or more threads are waiting to acquire the lock. In that case, once the thread with the lock updates the field, additional threads that show up wanting to enter the protected section of code will be using a different object than those that were waiting before. Thus, two or more threads could wind up executing the same code simultaneously.No, I don't think that locking on the delegate field itself would work.
  • Anonymous
    March 18, 2010
    @pete.dI knew I had to be missing something... now I feel really stupid for not seeing it ;)Thanks for the explanation !