.NET中Event引起的内存泄露

原文:Understanding and Avoiding Memory Leaks with Event Handlers and Event Aggregators

If you subscribe to an event in C# and forget to unsubscribe, does it cause a memory leak? Always? Never? Or only in special circumstances? Maybe we should make it our practice to always unsubscribe just in case there is a problem. But then again, the Visual Studio designer generated code doesn’t bother to unsubscribe, so surely that means it doesn’t matter?.

1 updater.Finished += new EventHandler(OnUpdaterFinished);
2 updater.Begin();
3  
4 ...
5  
6 // is this important? do we have to unsubscribe?
7 updater.Finished -= new EventHandler(OnUpdaterFinished);

Fortunately it is quite easy to see for ourselves whether any memory is leaked when we forget to unsubscribe. Let’s create a simple Windows Forms application that creates lots of objects, and subscribe to an event on each of the objects, without bothering to unsubscribe. To make life easier for ourselves, we’ll keep count of how many get created, and how many get deleted by the garbage collector, by reducing a count in their finalizer, which the garbage collector will call.Here’s the object we’ll be creating lots of instances of:

 1 public class ShortLivedEventRaiser
 2 {
 3     public static int Count;
 4      
 5     public event EventHandler OnSomething;
 6  
 7     public ShortLivedEventRaiser()
 8     {
 9         Interlocked.Increment(ref Count);
10     }
11  
12     protected void RaiseOnSomething(EventArgs e)
13     {
14         EventHandler handler = OnSomething;
15         if (handler != null) handler(this, e);
16     }
17  
18     ~ShortLivedEventRaiser()
19     {
20         Interlocked.Decrement(ref Count);
21     }
22 }

and here’s the code we’ll use to test it:

 1 private void OnSubscribeToShortlivedObjectsClick(object sender, EventArgs e)
 2 {
 3     int count = 10000;
 4     for (int n = 0; n < count; n++)
 5     {
 6         var shortlived = new ShortLivedEventRaiser();
 7         shortlived.OnSomething += ShortlivedOnOnSomething;
 8     }
 9     shortlivedEventRaiserCreated += count;
10 }
11  
12 private void ShortlivedOnOnSomething(object sender, EventArgs eventArgs)
13 {
14     // just to prove that there is no smoke and mirrors, our event handler will do something involving the form
15     Text = "Got an event from a short-lived event raiser";
16 }

I’ve added a background timer on the form, which reports every second how many instances are still in memory. I also added a garbage collect button, to force the garbage collector to do a full collect on demand.

So we click our button a few times to create 80,000 objects, and quite soon after we see the garbage collector run and reduce the object count. It doesn’t delete all of them, but this is not because we have a memory leak. It is simply that the garbage collector doesn’t always do a full collection. If we press our garbage collect button, we’ll see that the number of objects we created drops down to 0. So no memory leaks! We didn’t unsubscribe and there was nothing to worry about.

But let’s try something different. Instead of subscribing to an event on my 80,000 objects, I’ll let them subscribe to an event on my Form. Now when we click our button eight times to create 80,000 of these objects, we see that the number in memory stays at 80,000. We can click the Garbage Collect button as many times as we want, and the number won’t go down. We’ve got a memory leak!

Here’s the second class:

 1 public class ShortLivedEventSubscriber
 2 {
 3     public static int Count;
 4  
 5     public string LatestText { get; private set; }
 6  
 7     public ShortLivedEventSubscriber(Control c)
 8     {
 9         Interlocked.Increment(ref Count);
10         c.TextChanged += OnTextChanged;
11     }
12  
13     private void OnTextChanged(object sender, EventArgs eventArgs)
14     {
15         LatestText = ((Control) sender).Text;
16     }
17  
18     ~ShortLivedEventSubscriber()
19     {
20         Interlocked.Decrement(ref Count);
21     }
22 }

and the code that creates instances of it:

1 private void OnShortlivedEventSubscribersClick(object sender, EventArgs e)
2 {
3     int count = 10000;
4     for (int n = 0; n < count; n++)
5     {
6         var shortlived2 = new ShortLivedEventSubscriber(this);
7     }
8     shortlivedEventSubscriberCreated += count;
9 }

So why does this leak, when the first doesn’t? The answer is that event publishers keep their subscribers alive. If the publisher is short-lived compared to the subscriber, this doesn’t matter. But if the publisher lives on for the life-time of the application, then every subscriber will also be kept alive. In our first example, the 80,000 objects were the publishers, and they were keeping the main form alive. But it didn’t matter because our main form was supposed to be still alive. But in the second example, the main form was the publisher, and it kept all 80,000 of its subscribers alive, long after we stopped caring about them.

The reason for this is that under the hood, the .NET events model is simply an implementation of the observer pattern. In the observer pattern, anyone who wants to “observe” an event registers with the class that raises the event. It keeps hold of a list of observers, allowing it to call each one in turn when the event occurs. So the observed class holds references to all its observers.

What does this mean?

The good news is that in a lot of cases, you are subscribing to an event raised by an object whose lifetime is equal or shorter than that of the subscribing class. That’s why a Windows Forms or WPF control can subscribe to events raised by child controls without the need to unsubscribe, since those child controls will not live beyond the lifetime of their container.

When it goes wrong is when you have a class that will exist for the lifetime of your application, raising events whose subscribers were supposed to be transitory. Imagine your application has a order service which allows you to submit new orders and also has an event that is raised whenever an order’s status changes.

1 orderService.SubmitOrder(order);
2 // get notified if an order status is changed
3 orderService.OrderStatusChanged += OnOrderStatusChanged;

Now this could well cause a memory leak, as whatever class contains the OnOrderStatusChanged event handler will be kept alive for the duration of the application run. And it will also keep alive any objects it holds references to, resulting in a potentially large memory leak. This means that if you subscribe to an event raised by a long-lived service, you must remember to unsubscribe.

What about Event Aggregators?

Event aggregators offer an alternative to traditional C# events, with the additional benefit of completely decoupling the publisher and subscribers of events. Anyone who can access the event aggregator can publish an event onto it, and it can be subscribed to from anyone else with access to the event aggregator.

But are event aggregators subject to memory leaks? Do they leak in the same way that regular event handlers do, or do the rules change? We can test this out for ourselves, using the same approach as before.

For this example, I’ll be using an extremely elegant event aggregator built by José Romaniello using Reactive Extensions. The whole thing is implemented in about a dozen of code thanks to the power of the Rx framework.

First, we’ll simulate many short-lived publishers with a single long-lived subscriber (our main form). Here’s our short-lived publisher object:

 1 public class ShortLivedEventPublisher
 2 {
 3     public static int Count;
 4     private readonly IEventPublisher publisher;
 5  
 6     public ShortLivedEventPublisher(IEventPublisher publisher)
 7     {
 8         this.publisher = publisher;
 9         Interlocked.Increment(ref Count);
10     }
11  
12     public void PublishSomething()
13     {
14         publisher.Publish("Hello world");
15     }
16  
17     ~ShortLivedEventPublisher()
18     {
19         Interlocked.Decrement(ref Count);
20     }
21 }

And we’ll also try many short-lived subscribers with a single long-lived publisher (our main form):

 1 public class ShortLivedEventBusSubscriber
 2 {
 3     public static int Count;
 4     public string LatestMessage { get; private set; }
 5  
 6     public ShortLivedEventBusSubscriber(IEventPublisher publisher)
 7     {
 8         Interlocked.Increment(ref Count);
 9         publisher.GetEvent<string>().Subscribe(s => LatestMessage = s);
10     }
11  
12     ~ShortLivedEventBusSubscriber()
13     {
14         Interlocked.Decrement(ref Count);
15     }
16 }

What happens when we create thousands of each of these objects?

We have exactly the same memory leak again – publishers can be garbage collected, but subscribers are kept alive. Using an event aggregator hasn’t made the problem any better or worse. Event aggregators should be chosen for the architectural benefits they offer rather than as a way to fix your memory management problems (although as we shall see shortly, they encapsulate one possible fix).

How can I avoid memory leaks?

So how can we write event-driven code in a way that will never leak memory? There are two main approaches you can take.

1. Always remember to unsubscribe if you are a short-lived object subscribing to an event from a long-lived object. The C# language support for events is less than ideal. The C# language offers the += and -= operators for subscribing and unsubscribing, but this can be quite confusing.Here’s how you would unsubscribe from a button click handler…

1 button.Clicked += new EventHandler(OnButtonClicked)
2 ...
3 button.Clicked –= new EventHandler(OnButtonClicked)

It’s confusing because the object we unsubscribe with is clearly a different object to the one we subscribed with, but under the hood .NET works out the right thing to do. But if you are using the lambda syntax, it is a lot less clear what goes on the right hand side of the –= (see this stack overflow question for more info). You don’t exactly want to keep trying to replicate the same lambda statement in two places.

1 button.Clicked += (sender, args) => MessageBox.Show(“Button was clicked”);

This is where event aggregators can offer a slightly nicer experience. They will typically have an “unregister” or an “unsubscribe” method. The Rx version I used above returns an IDisposable object when you call subscribe. I like this approach as it means you can either use it in a using block, or store the returned value as a class member, and make your class Disposable too, implementing the standard .NET practice for resource cleanup and flagging up to users of your class that it needs to be disposed.

2. Use weak references. But what if you don’t trust yourself, or your fellow developers to always remember to unsubscribe? Is there another solution? The answer is yes, you can use weak references. A weak reference holds a reference to a .NET object, but allows the garbage collector to delete it if there are no other regular references to it.

The trouble is, how do you attach a weak event handler to a regular .NET event? The answer is, with great difficulty, although some clever people have come up withingenious ways of doing this. Event aggregators have an advantage here in that they can offer weak references as a feature if wanted, hiding the complexity of working with weak references from the end user. For example, the “Messenger” class that comes with MVVM Light uses weak references.

So for my final test, I’ll make an event aggregator that uses weak references. I could try to update the Rx version, but to keep things simple, I’ll just make my own basic (and not threadsafe) event aggregator using weak references. Here’s the code:

 1 public class WeakEventAggregator
 2 {
 3     class WeakAction
 4     {
 5         private WeakReference weakReference;
 6         public WeakAction(object action)
 7         {
 8             weakReference = new WeakReference(action);
 9         }
10  
11         public bool IsAlive
12         {
13             get { return weakReference.IsAlive; }
14         }
15  
16         public void Execute<TEvent>(TEvent param)
17         {
18             var action = (Action<TEvent>) weakReference.Target;
19             action.Invoke(param);
20         }
21     }
22  
23     private readonly ConcurrentDictionary<Type, List<WeakAction>> subscriptions
24         = new ConcurrentDictionary<Type, List<WeakAction>>();
25  
26     public void Subscribe<TEvent>(Action<TEvent> action)
27     {
28         var subscribers = subscriptions.GetOrAdd(typeof (TEvent), t => new List<WeakAction>());
29         subscribers.Add(new WeakAction(action));
30     }
31  
32     public void Publish<TEvent>(TEvent sampleEvent)
33     {
34         List<WeakAction> subscribers;
35         if (subscriptions.TryGetValue(typeof(TEvent), out subscribers))
36         {
37             subscribers.RemoveAll(x => !x.IsAlive);
38             subscribers.ForEach(x => x.Execute<TEvent>(sampleEvent));
39         }
40     }
41 }

Now let’s see if it works by creating some short-lived subscribers that subscribe to events on the WeakEventAggregator. Here are the objects, we’ll be using in this last example:

 1 public class ShortLivedWeakEventSubscriber
 2 {
 3     public static int Count;
 4     public string LatestMessage { get; private set; }
 5  
 6     public ShortLivedWeakEventSubscriber(WeakEventAggregator weakEventAggregator)
 7     {
 8         Interlocked.Increment(ref Count);
 9         weakEventAggregator.Subscribe<string>(OnMessageReceived);
10     }
11  
12     private void OnMessageReceived(string s)
13     {
14         LatestMessage = s;
15     }
16  
17     ~ShortLivedWeakEventSubscriber()
18     {
19         Interlocked.Decrement(ref Count);
20     }
21 }

And we create another 80,000, do a garbage collect, and finally we can have event subscribers that don’t leak memory:

Conclusion

Although many (possibly most) use cases of events do not leak memory, it is important for all .NET developers to understand the circumstances in which they might leak memory. I’m not sure there is a single “best practice” for avoiding memory leaks. In many cases, simply remembering to unsubscribe when you are finished wanting to receive messages is the right thing to do. But if you are using an event aggregator you’ll be able to take advantage of the benefits of weak references quite easily.

.NET中Event引起的内存泄露,古老的榕树,5-wow.com

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。