[本文转自《Solving the Problem with Events: Weak Event Handlers》March 26, 2007: Added performance tests, corrected several typos and clarified the performance issues with the lightweight code generation approach.]
1、The Problem (A Recap)
As discussed last time, delegates can produce memory leaks in our applications if not used carefully. Most often, this happens with events when we add a handler to an event, forget to remove it and the object on which the event is declared lives longer than the object that handles it. The following diagram illustrates the problem.
In the diagram above, there is an object ("eventExposer") that declares an event ("SpecialEvent"). Then, a form is created ("myForm") that adds a handler to the event. The form is closed and the expectation is that the form will be released to garbage collection but it isn't. Unfortunately, the underlying delegate of the event still maintains a strong reference to the form because the form's handler wasn't removed.
The question is, how do we fix this problem?
Most of you are probably saying "Fix the form you idiot! Remove the handler!" That might seem reasonable—especially if we have control over all of the code. But what if we don't? What if this is a large plug-in based architecture where the event is surfaced by some event service that exists for the lifetime of the application and the form is in a third-party plug-in for which we don't have the code? In this scenario, we would have a leaky application that couldn't be corrected by simply modifying the form to properly remove the handler on close.
As I mentioned in my before, the ideal solution would be a CLR-level mechanism for creating "weak delegates" which hold weak references to their target objects instead of strong references. Unfortunately, such a mechanism doesn't yet exist (and won't for the foreseeable future) so we have to roll our own.
Greg Schechter presented a well-known solution a couple of years ago that is used in Avalon (err... WPF). This solution works perfectly except that it isn't all that convenient to use. It requires the declaration of a new class that has intimate knowledge of both the container and containee (or subject and observer). Schechter's essay inspired several developers in the community to come up with architectures that make this approach easier to use and a couple of articles were submitted to code project. Notably, "Observable property pattern, memory leaks, and weak delegates for .NET" and "An Easy to Use Weak Referenced Event Handler Factory for .NET 2.0". Unfortunately, neither of these solutions are ideal. First, both of them have several support classes. In addition, the first is extraordinarily heavy because it uses Refelection.Emit to generate types and assemblies on the fly (more on this type of thing later) and the second one requires extra client code and uses Reflection to add add and remove handlers from an event. I should mention however that both solutions do work.
Over the past few years, the community has offered several "magic class" approaches (where a special class is instantiated that wraps your event handler). A "magic class" is an attractive solution because it results in far less client code. Unfortunately, the majority of these offerrings got it wrong. Most of them make the mistake of creating a weak reference to the event handler itself rather than the target of the event handler. Shawn A. Van Ness eventually removed his solution because of problems. Ian Griffiths kept his available with a note that it doesn't actually work. Even Joe Duffy (of whom I'm a big fan) presented a "magic class"-type solution that doesn't really do the job. A very interesting approach that does work is the one presented by Xavier Musy. Xavier chose to essentially re-implement System.MulticastDelegate but it suffers from performance issues and is not safe multi-threaded scenarios. Finally, Gregor R. Peisker presented a real working (and performant) solution just a few months ago that takes advantage of a technique that is crucial to getting this right.
I have to admit, there are several obstacles to creating a working solution. First of all, you can't simply inherit from System.Delegate or System.MulticastDelegate and override the appropriate methods. That would be great but it simply isn't possible. You can't even do it IL (believe me—I've tried). In addition, there isn't a generic constraint for delegates. We can work around that but it certainly makes some code that we might write less elegant.
However, the biggest obstacle is performance. There are two areas of performance that we can consider: 1) adding/removing a handler and 2) actually invoking the handler. On the average, invocation occurs far more often than adding and removing handlers so we'll focus our attention on that. There are a lot of ways to dynamically invoke a delegate and some of them are very slow when compared with a normal typed delegate invocation. Using a weak delegate or event handler is expected to have some overhead but we should keep it to a minimum.
Another potentially serious issue is removing the handler. Let me explain what I mean. When we use a "magic class" to wrap an event handler and add it to an event, we create a weak reference to the event handler's target but the event still maintains a strong reference to our wrapper object. So, when the target object is garbage collected, the wrapper object is kept alive for the same reason that the target wasn't being garbage collected before. Granted, it will likely take far less memory than some large object (e.g. the form in the diagram that I mentioned earlier) but it's still a leak. That might be acceptable to some of you but there's actually a very simple solution to this so we should deal with it properly.
The last obstacle that I want to mention is how to create a class that will handle any delegate type. I'm going to present a solution that work with any delegate but we can get much higher-performance if we can use a specific delegate type. Because of that, I'll focus most of my attention on that only work with the handy generic System.EventHandler<TEventArgs> delegate from the .NET 2.0 framework.
4、A First Stab
OK, let's dig into some code and take a shot at creating a "magic class" so that I can introduce the players. Here's a very a naive implementation:
This approach would actually work in very simple delegate scenarios but it isn't robust enough to be used with an event. However, it serves to demonstrate how we can use the System.WeakReference clsss to create a weak reference to the delegate's target. To use, instantiate a new WeakReference and pass the object that you want to track to its constructor. Then, you can check the WeakReference.IsAlive or WeakReference.Target properties to see if the object has been garbage collected. Keep in mind that you should never write code like this:
I've seen way too many developers (including myself many moons ago) make this mistake. The problem is that the garbage collector runs on a different thread. This causes a race condition to occur between the calls to m_TargetRef.IsAlive and m_TargetRef.Target. In other words, m_TargetRef.IsAlive could return true but the garbage collector could kick in and reclaim the target instance before m_TargetRef.Target is called, potentially causing a NullReferenceException to be thrown. The proper way to use this class is to store the value of m_TargetRef.Target in a local variable. If m_TargetRef.Target doesn't return null, the local variable creates a strong reference to the target instance that can safely be used.
So, using the System.WeakReference is fairly simple but it has one non-obvious problem that I'd like to highlight: it is finalizable but does not implement IDisposable. What does that mean and why is it bad? It means that any System.WeakReference instance will add a small amount of pressure to the garbage collector because its finalizer method must be called before it can be reclaimed. Internally, WeakReference uses a System.GCHandle to track the target object and it declares a finalizer to clean up that GCHandle. If it implemented IDisposable properly, we could call the Dispose method when finished with the WeakReference. That would release the GCHandle and call GC.SuppressFinalize(this) to keep the finalizer from being called (thus removing the GC pressure). (In CLR via C#, Jeffrey Richter refers to this lack of an IDisposable implementation as a bug.) Now, we could use System.GCHandle ourselves instead of System.WeakReference but there is a problem with that. The public methods on GCHandle require a link demand for the unmanaged code security permission. That means that our WeakDelegate class would need the same permission. WeakReference actually gets around those security permissions by using internal methods on GCHandle that don't declare them (it does have an inheritance demand though—beware inheritors!). Because it requires less security checks, using WeakReference is actually a little bit faster. Sigh...
There is one other important player from our initial attempt that I'd like to point out. The delegate is invoked by calling MethodInfo.Invoke() and passing the target instance and the arguments. This is really slow. Directly invoking the delegate would be much faster but we can't store a reference to the original delegate because it strongly references the target. So, we have to rely upon late-bound mechanisms that are much slower. This is the main reason that we will shift our focus from wrapping System.Delegate to System.EventHandler<TEventArgs> shortly.
The robustness of this solution can be improved by making WeakDelegate multi-casting. Currently, it only supports single delegates which is why it is insufficient for using with events. To do this, we would need to use a similar approach to the one presented by Xavier Musy. The idea is that we add Combine and Remove static methods that are equivalent to those that are found on System.Delegate. With these in place, we could use this class to declare a weak event like this:
The main issue here is that all handlers are weak—whether they need to be or not. It would be more flexible to have a weak delegate class that can be used like an ordinary delegate. That way, a subscriber could make specific handlers weak and other handlers not. The ideal syntax for a subscriber might look something like this:
5、The Second Attempt
At this point, we're in a bit of a pickle if we want to continue trying to provide a solution that wraps any System.Delegate. In fact, to go down this road, we have use the System.Reflection.Emit classes to generate a new assembly, a new module, a new type, a WeakReference field, a new method that matches the signature of the delegate and all of the IL necessary to access the WeakReference.Target property, load the parameters onto the stack and call the method referenced by the delegate. Now, this is certainly possible to do (yes, I did it and it made me weep like a child) but the result is a mechanism that is so heavy that it's almost worthless. Plus, the assembly, module and type are not reclaimable by garbage collection unless you actually create a new AppDomain to contain the code so it can be unloaded later. However, using another AppDomain will cause invocations of the delegate to cross AppDomain boundaries (ahem... remoting) and it is fraught with peril because you need to work out some serialization mechanism for the delegate and its target. Are you beginning to realize why I'm not showing any code here? Obviously, this approach kills both performance and memory use. And, since we're trying to create something that's both fast and lightweight, this is simply not acceptable.
OK, so I'm going to abandon the idea of creating a magic class that works with any delegate type and focus on creating a solution for a specific delegate type: System.EventHandler<TEventArgs>. That will limit the solution to .NET 2.0-only but that's ok. The techniques that we'll employ are 2.0 anyway. Microsoft actually recommends that you use EventHandler<TEventArgs> delegate for declaring events if you are targeting .NET 2.0+ anyway. (Microsoft even managed to follow their own recommendation and used this delegate type throughout WCF and WF. They seemingly forgot about WPF though. Sigh...)
With a specific signature, it is much easier to create a "magic" WeakEventhandler class that achieves the syntax that we're looking for.
This version is starting to look a little more like what we want. Being tied to a specific delegate type, we know what the method signature will be and we can create our own method that matches to use as a sort of "delegate proxy". And, thanks to the implicit conversion we can write the golden syntax that we're looking for.
I ran a simple performance test that adds and fires 100 event handlers in the standard way and with the WeakEventHandler class. Here are the results:
Obviously, this approach is pretty slow. Invocation through MethodInfo.Invoke() is nearly 6 times slower than normal invocation.
This leaves us with two problems left to solve:
- Invocation performance. We're still using MethodInfo.Invoke() and it's slow.
- Removal of the weak event handler from the event after the target has been garbage collected.
The second problem is actually very simple so let's look at the performance issue first.
Using MethodInfo.Invoke() is really slow. What we'd really like is some way to generate a delegate that does not actually store a reference to its target but allows us to specify the target when we call it. That way, our invocation performance characteristic is very similar to that of a standard delegate invocation. In .NET 1.1 there really wasn't a way to create such a delegate but there are two options available to us (that I am aware of) in .NET 2.0.
The first option that I want to explore is lightweight code generation (LCG). Lightweight code generation is a mechanism in the .NET 2.0 framework that allows methods to be generated on the fly without the requirement of an assembly, module, type, etc. These "dynamic methods" can be associated with a specific module (making them effectively global) or a specific type (making them static). Also, unlike the Reflection.Emit story in 1.0/1.1, they have the benefit of being reclaimable by the garbage collector. In fact, the only real downside of LCG is that you have to spit out the IL of the dynamic method. There is a small hit when creating a dynamic method but, as discussed earlier, the invocation performance is far more important.
Clearly, this code is far more complicated than when it used MethodInfo.Invoke(). First, there is a new delegate declared ("EventHandlerThunk") which has the same signature as the EventHandler that we're wrapping with an additional parameter to take the target. We create an EventHandlerThunk as a DynamicMethod and when invoking it, we pass the target along with the parameters (see the Invoke method above). Most of the magic code is in the CreateDynamicThunk method. I don't want to get too detailed about the IL that is generated but it essentially calls the method referenced by "eventHandler" directly instead of through the MethodInfo.Invoke(). Very snazzy.
Using LCG, the invocation performance is not great. I ran the same test that I ran earlier and got these results:
Whoa! It turns out that that invoking the LCG solution is almost 58 times slower than normal invocation! Obviously, this is a step backward and not really what we're looking for. Part of this poor performance is caused by the fact that we're actually creating three method calls here: WeakEventHandler.Invoke() calls the dynamic EventHandlerThunk which calls the real method. The really problem, however, is that the "lightweight" in "lightweight code generation" really refers to memory and not performance. So, this isn't an acceptable solution for is.
The best option that is available to us was mentioned by Joe Duffy in a comment on another blog. Instead of creating a DynamicMethod and generating IL, we can use an open instance or unbound delegate.
Now, some of you might be scratching your heads right now and saying, "what's an open instance delegate and why haven't I heard of them before?"
An open instance delegate is a delegate that references an instance method but doesn't reference a target. Instead the target is to be specified when the delegate is called. To allow for this, the delegate type of the open instance delegate must declare an additional parameter that takes the instance that the delegate is called on when invoked (just like our EventHandlerThunk delegate type).
That explains what they are but why haven't you heard of them? Well, there's no language support for them in C# and VB. In order to create one, you have to use an overload of the Delegate.CreateDelegate() method. Open instance delegates were designed to support STL.NET and C++/CLI (where they're called unbound delegates). Additionally, nobody has really gotten excited about them except for Gregor R. Peisker. His working solution that I mentioned earlier uses an open instance delegate.
Here is the code reworked to use an open instance delegate:
Oops! That compiles but it doesn't actually work. When a WeakEventHandler is instantiated, the call to Delegate.CreateDelegate fails with a System.ArgumentException and the (unhelpful) message: "Error binding to the target method." What the heck is going on? Well, if you look at the documentation for unbound delegates in C++ it states that "the first parameter of the delegate signature is the type of this for the object you want to call." In other words, using "object" as the type of the first parameter won't work. It has to match. To correct this problem, we have to add another generic type parameter to our WeakEventHandler class to represent the type of the instance on which the handler is declared. Here's the new version:
This works perfectly but now we've broken our client code syntax. The second generic type parameter has to be specified like this:
I really dislike the additional generic type parameter because it requires cerebral activity on the part of the user to determine what type needs to be filled in. But, we've corrected the problem that we set out to fix: invocation performance. There is a performance bump when the open instance delegate is created but that's acceptable because the invocation performance is very good.
Ideally, our WeakEventHandler would be automatically removed from the event on which it is registered when its target is reclaimed by the garbage collector. Unfortunately, that's not possible. There simply isn't any sort of notification when a garbage collection occurs (unless you're using the CLR hosting, debugging or profiling APIs). Because of this, we have to enlist an idea from Joe Duffy and do the next best thing: we pass a delegate to the WeakEventHandler's constructor that will be called during invocation if the target has been garbage collected. Here's the code:
If we pass an UnregisterCallback into the constructor of WeakEventHandler, it will be called once if the WeakEventHandler is invoked after the target has been garbage collected. That neatly solves the problem. Of course, our WeakEventHandler leaks if the event never fires again but I think that's acceptable. Here is what the client code looks like now:
Now that we have a working "magic class" solution, let's take a look at how we might improve the syntax.
8、Making It Pretty
The most important thing is to get rid of the additional generic type parameter that we were forced to add. It just requires too much thinking to assign it properly. The use of an anonymous method might confuse the code for some but it's easy enough to move that into a separate method.
To remove the generic type parameter from the client code, we'll have to resort to reflection to construct the WeakEventHandler<T, E> type (because we have to fill that type parameter in somehow). Gregor R. Peisker's solution employs this trick so we can use it as a model. Here's my version:
That looks like a lot more code but it's not too bad. Our magic class has remained relatively unchanged except that the UnregisterCallback delegate type is no longer nested inside and it has a Handler property to implement the new IWeakEventHandler<E> interface. The meat is in EventHandlerUtils.MakeWeak<E>(). This generic method performs the reflection magic necessary to instantiate WeakEventHandler<T, E> with its generic type parameters dynamically filled in. This is where the IWeakEventHandler<E> interface becomes handy. Without it, we would have to use reflection to access the m_Handler field.
At this point, I expect that you're wondering what the performance characteristics of this approach are. Well, using the same performance test with the EventHandlers.MakeWeak<E>() method, I got the following results:
This is a much better performance story and closer to what we're looking for. This test demonstrates that using an open instance delegate in the WeakEventHandler class results in performance that is only about 2.5 times slower than standard delegate invocation. That is actually about what we should expect since a WeakEventHandler causes two delegate invocations.
There is room for improvement in EventHandlerUtils.MakeWeak(). A final version could cache the ConstructorInfo (probably using the .NET 2.0 reflection token APIs) and use the values of the two generic type parameters as a composite key. Also, a dynamic method might be employed (and cached) so that ConstructorInfo.Invoke() doesn't have to be called. So, there are several tweaks available.
We have improved the client code syntax a bit. It's easier to use now:
The biggest syntax improvement occurs with C# 3.0. When calling EventHandlerUtils.MakeWeak in C# 3, we can use a lambda expression instead of an anonymous method like this:
Even better, the EventHandlerUtils.MakeWeak method can be turned into an extension method for EventHandler<TEventArgs>. Then we can call it like this:
Now that's syntax that I can really get behind. If you're wondering how you might use this from within the class that declares an event to make all subscribed event handlers weak, here's one possibility: