The .NET weak event pattern in C#
Introduction
As you may know event handlers are a common source of memory leaks caused by the persistence of objects that are not used anymore, and you may think should have been collected, but are not, and for good reason.
In this (hopefully) short article, I’ll present the issue with event handlers in the context of the .Net framework, then I’ll show you how you can implement the standard solution to this issue, the weak event pattern, in two ways, either using:
- the “legacy” (well, before .Net 4.5, so not that old) approach which is quite cumbersome to implement
- the new approach provided by the .Net 4.5 framework which is as simple as it can be
(The source code is available here.)
The common stuff
Before diving into the core of the article let’s review two items which are used extensively in the code: a class and a method.
The event-source
Let me present you a basic but useful event-source class which exposes just enough complexity to illustrate the point:
public class EventSource
{
public event EventHandler<EventArgs> Event = delegate { }; public void Raise()
{
Event(this, EventArgs.Empty);
}
}
For those who wonder what the strange empty delegate initialization is: it’s a trick to be sure the event is always initialized, without having to check each time if it’s non-null before using it.
The GC triggering utility method
In .Net the garbage collection is triggered in a non-deterministic manner, which is not good for our tests that need to track the state of objects in a deterministic manner.
So we’ll regularly have to trigger ourselves a GC, and to avoid duplicating plumbing code it’s been factorized in a dedicated method:
static void TriggerGC()
{
Console.WriteLine("Starting GC."); GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect(); Console.WriteLine("GC finished.");
}
Not rocket science but it deserves a little explanation if you’re not familiar with this pattern:
- first
GC.Collect()triggers the .Net CLR garbage-collector which will take care of sweeping unused objects, and for objects whose class has no finalizer (a.k.a destructor in C#) it’s enough GC.WaitForPendingFinalizers()waits for the finalizers of other objects to execute; we need it because as you’ll see we’ll use the finalizer methods to know when our objects are collected- second
GC.Collect()ensures the newly finalized objects are swept too.
The issue
So first thing first, let’s try to understand what’s the problem with event listeners, with the help of some theory and, most importantly, a demo.
Background
When an object acting as an event listener registers one of its instance methods as an event handler on an object that produces events (the event source), the event source must keep a reference to the event listener object in order to raise the event in the context of this listener.
This is fair enough, but if this reference is a strong reference then the listener acts as a dependency of the event source and can’t be garbage-collected even if the last object referencing it is the event source.
Here is a detailed diagram of what happens under the hood:
Events handlers issue
This is not an issue if you can control the life time of the listener object as you can unsubscribe from the event source when you don’t need the listener anymore, typically using the disposable pattern.
But if you can’t identify a single point of responsibility for the life time of the listener then you can’t dispose of it in a deterministic manner and you have to rely on the garbage collection process … which will never consider your object as ready for collection as long as the event source is alive!
Demo
Theory is all good but let’s see the issue with real code.
Here is our brave event listener, just a bit naive, and we’ll quickly understand why:
public class NaiveEventListener
{
private void OnEvent(object source, EventArgs args)
{
Console.WriteLine("EventListener received event.");
} public NaiveEventListener(EventSource source)
{
source.Event += OnEvent;
} ~NaiveEventListener()
{
Console.WriteLine("NaiveEventListener finalized.");
}
}
Let’s see how this implementation behaves with a simple use-case:
Console.WriteLine("=== Naive listener (bad) ===");
EventSource source = new EventSource();
NaiveEventListener listener = new NaiveEventListener(source);
source.Raise();
Console.WriteLine("Setting listener to null.");
listener = null;
TriggerGC();
source.Raise();
Console.WriteLine("Setting source to null.");
source = null;
TriggerGC();
Here is the output:
EventListener received event.
Setting listener to null.
Starting GC.
GC finished.
EventListener received event.
Setting source to null.
Starting GC.
NaiveEventListener finalized.
GC finished.
Let’s analyze the workflow:
- “EventListener received event.“: this is the consequence of our call to “
source.Raise()”; perfect, seems like we’re listening. - “Setting listener to null.“: we nullify the reference that the current local context holds to the event listener object, which should allow garbage collection of the listener.
- “Starting GC.“: garbage collection starts.
- “GC finished.“: garbage collection ends, but our event listener object has not been reclaimed by the garbage collector, which is proven by the fact its finalizer has not been called
- “EventListener received event.“: this is confirmed by the second call to “
source.Raise()”, the listener is still alive! - “Setting source to null.“: we nullify the reference to the event source object.
- “Starting GC.“: the second garbage collection starts.
- “NaiveEventListener finalized.“: this time our naive listener is collected, better late than never.
- “GC finished.“: the second garbage collection ends.
Conclusion: indeed there is an hidden strong reference to the listener which prevents the event listener to be collected as long as the event source is not collected!
Hopefully there is a standard solution for this issue: the event source can reference the listener through a weak reference, which won’t prevent collection of the listener even if the source is still alive.
And there is a standard pattern and its implementation in the .Net framework: the weak event pattern.
The weak event pattern
So let’s see how we can tackle the issue in the .Net framework.
As often there is more than one way to do it, but in this case the decision process is quite straightforward:
- if you’re using .Net 4.5 you can benefit from a simple implementation
- otherwise you’ll have to rely on a slightly more contrived approach
The legacy way
Before .Net 4.5 the .Net framework came with a class and an interface that allowed implementation of the weak event pattern:
- WeakEventManager which is where all the pattern plumbing is encapsulated
- IWeakEventListener which is the pipe that allows a component to connect to the
WeakEventManagerplumbing
(Both are located in the WindowsBase assembly that you’ll need to reference yourself if you’re not developing a WPF project which should already correctly reference it.)
So this is a two step process.
First you implement a custom event manager by specializing the WeakEventManager class:
- you override the StartListening and StopListening methods that respectively registers a new handler and unregisters an existing one; they will be used by the
WeakEventManagerbase class itself - you provide two methods to give access to the listeners list, typically named “
AddListener” and “RemoveListener“, that are intended for the users of your custom event manager - you provide a way to get an event manager for the current thread, typically by exposing a static property on your custom event manager class
Then you make your listener class implement the IWeakEventListener interface:
- you implement the ReceiveWeakEvent method
- you try to handle the event
- you return
trueif you’ve been able to handle the event correctly
This is a lot of words, but it translates to relatively few code:
First the custom weak event manager:
Copy Codepublic class EventManager : WeakEventManager
{
private static EventManager CurrentManager
{
get
{
EventManager manager = (EventManager)GetCurrentManager(typeof(EventManager)); if (manager == null)
{
manager = new EventManager();
SetCurrentManager(typeof(EventManager), manager);
} return manager;
}
} public static void AddListener(EventSource source, IWeakEventListener listener)
{
CurrentManager.ProtectedAddListener(source, listener);
} public static void RemoveListener(EventSource source, IWeakEventListener listener)
{
CurrentManager.ProtectedRemoveListener(source, listener);
} protected override void StartListening(object source)
{
((EventSource)source).Event += DeliverEvent;
} protected override void StopListening(object source)
{
((EventSource)source).Event -= DeliverEvent;
}
}
Then our event listener:
public class LegacyWeakEventListener : IWeakEventListener
{
private void OnEvent(object source, EventArgs args)
{
Console.WriteLine("LegacyWeakEventListener received event.");
} public LegacyWeakEventListener(EventSource source)
{
EventManager.AddListener(source, this);
} public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
OnEvent(sender, e); return true;
} ~LegacyWeakEventListener()
{
Console.WriteLine("LegacyWeakEventListener finalized.");
}
}
Let’s check it:
Console.WriteLine("=== Legacy weak listener (better) ===");
EventSource source = new EventSource();
LegacyWeakEventListener listener = new LegacyWeakEventListener(source);
source.Raise();
Console.WriteLine("Setting listener to null.");
listener = null;
TriggerGC();
source.Raise();
Console.WriteLine("Setting source to null.");
source = null;
TriggerGC();
Results:
LegacyWeakEventListener received event.
Setting listener to null.
Starting GC.
LegacyWeakEventListener finalized.
GC finished.
Setting source to null.
Starting GC.
GC finished.
Nice, it works, our event listener object is now correctly finalized during the first GC though the event source object is still alive, no more leak.
But this is quite a bunch of code to write for a simple listener, imagine you have dozens of such listeners, you’d have to write a new weak event manager for each type!
If you are fluent with code refactoring and generics you may have found a clever way of refactoring all this common code.
Before .Net 4.5 you had to implement this clever weak event manager yourself, but now .Net provides a standard solution for this issue, and we’ll review it right now!
The .Net 4.5 way
.Net 4.5 has introduced a new generic version of the legacy WeakEventManager:WeakEventManager<TEventSource, TEventArgs>.
(This class is located in the WindowsBase assembly too.)
Thanks to a good use of .Net generics the WeakEventManager<TEventSource, TEventArgs> handlesgenericity itself, without us having to reimplement a new manager for each event source.
As a consequence the resulting code is far lighter and readable:
public class WeakEventListener
{
private void OnEvent(object source, EventArgs args)
{
Console.WriteLine("WeakEventListener received event.");
} public WeakEventListener(EventSource source)
{
WeakEventManager<EventSource, EventArgs>.AddHandler(source, "Event", OnEvent);
} ~WeakEventListener()
{
Console.WriteLine("WeakEventListener finalized.");
}
}
There is only a single line of code to write, really clean.
The usage is similar to the other implementations, as all the stuff has been encapsulated into the event listener class:
Console.WriteLine("=== .Net 4.5 weak listener (best) ===");
EventSource source = new EventSource();
WeakEventListener listener = new WeakEventListener(source);
source.Raise();
Console.WriteLine("Setting listener to null.");
listener = null;
TriggerGC();
source.Raise();
Console.WriteLine("Setting source to null.");
source = null;
TriggerGC();
And just to be sure it works as advertised here is the output:
WeakEventListener received event.
Setting listener to null.
Starting GC.
WeakEventListener finalized.
GC finished.
Setting source to null.
Starting GC.
GC finished.
As expected the behavior is the same as the legacy event manager, what more could we ask for?!
Conclusion
As you’ve seen implementing the weak event pattern in .Net is quite straightforward, particularly with .Net 4.5.
If you’re not using .Net 4.5, as the implementation requires some boilerplate code, you may be tempted to not use this pattern and instead directly use the C# language facilities (+= and -=), and see if you have any memory issue, and only if you notice some leaks then make the necessary effort of implementing it.
But with .Net 4.5, as it’s almost free, the plumbing code being managed by the framework, you can really use it in the first place, though it’s a little less cool than the C# syntax “+=” and “-=” but semantics is equally clear, and this is what matters.
I’ve done my best to be technically accurate and avoid any spelling errors but if you catch any typo or mistake, have some issue with the code or have additional questions feel free to let a comment.
The .NET weak event pattern in C#的更多相关文章
- Weak Event Patterns
https://msdn.microsoft.com/en-US/library/aa970850(v=vs.100).aspx In applications, it is possible tha ...
- WPF: 深入理解 Weak Event 模型
在之前写的一篇文章(XAML: 自定义控件中事件处理的最佳实践)中,我们曾提到了在 .NET 中如果事件没有反注册,将会引起内存泄露.这主要是因为当事件源会对事件监听者产生一个强引用,导致事件监听者无 ...
- c#弱事件(weak event)
传统事件publisher和listener是直接相连的,这样会对垃圾回收带来一些问题,例如listener已经不引用任何对象但它仍然被publisher引用 垃圾回收器就不能回收listener所占 ...
- 【转载】详细解读C#中的 .NET 弱事件模式
你可能知道,事件处理是内存泄漏的一个常见来源,它由不再使用的对象存留产生,你也许认为它们应该已经被回收了,但不是,并有充分的理由. 在这个短文中(期望如此),我会在 .Net 框架的上下文事件处理中展 ...
- 剖析WPF数据绑定机制
引言 WPF框架采取的是MVVM模式,也就是数据驱动UI,UI控件(Controls)被严格地限制在表示层内,不会参与业务逻辑的处理,只是通过数据绑定(Data Binding)简单忠实地表达与之绑定 ...
- C#中的 .NET 弱事件模式
引言 你可能知道,事件处理是内存泄漏的一个常见来源,它由不再使用的对象存留产生,你也许认为它们应该已经被回收了,但不是,并有充分的理由. 在这个短文中(期望如此),我会在 .Net 框架的上下文事件处 ...
- WPF架构分析
1.DisptcherObject提供了线程和并发模型,实现了消息系统. 2.DependencyObject提供了更改通知,实现了绑定,样式. 3.Visual是托管API和非托管API(milco ...
- 深入理解.NET/WPF内存泄漏
众所周知,内存管理和如何避免内存泄漏(memory leak)一直是软件开发的难题.不要说C.C++等非托管(unmanaged)语言,即使是Java..NET等托管(managed)语言,尽管有着完 ...
- FlinkCEP - Complex event processing for Flink
https://ci.apache.org/projects/flink/flink-docs-release-1.3/dev/libs/cep.html 首先目的是匹配pattern sequenc ...
随机推荐
- 【API】文件操作编程基础-CreateFile、WriteFile、SetFilePointer
1.说明 很多黑客工具的实现是通过对文件进行读写操作的,而文件读写操作实质也是对API函数的调用. 2.相关函数 CreateFile : 创建或打开文件或I/O设备.最常用的I/O设备如下:文件,文 ...
- umount /mnt/cdrom
这是因为有程序正在访问这个设备,最简单的办法就是让访问该设备的程序退出以后再umount.可能有时候用户搞不清除究竟是什么程序在访问设备,如果用户不急着umount,则可以用: umount -l / ...
- MySQL的Auto-Failover功能
今天来体验一下MySQL的Auto-Failover功能,这里用到一个工具MySQL Utilities,它的功能很强大.此工具提供如下功能:(1)管理工具 (克隆.复制.比较.差异.导出.导入)(2 ...
- 在Docker中运行EOS(MAC版)
在Docker中运行EOS(MAC版) 在Docker中也可以简单快速的构建EOS.IO.笔者在Mac平台下参考官方文档躺了一次河.记录如下: 安装依赖 Docker 版本 17.05或者更高 tes ...
- Jmeter之逻辑控制器(Logic Controller)【转】
Jmeter之逻辑控制器(Logic Controller) 前言: 1. Jmeter官网对逻辑控制器的解释是:“Logic Controllers determine the order in w ...
- activiti helloworld
activiti helloworld activiti的入门实践文章,重点在于动手做,要解决的是怎么做的问题.只有知道了怎么做后,才具有实际动手能力,才算对这门技术有一个初步掌握:至于更深入细化的知 ...
- Java 接口关键字 interface
interface这个关键字产生一个完全抽象的类,它根本就没有提供任何具体的实现,它允许创建者确定方法名.参数列表和返回类型,但没有任何方法体,接口只提供了形式,而未提供任何具体实现 一个接口表示:& ...
- tensorflow-作用域
变量名字由两部分组成:scope/变量name. name 参数才是对象的唯一标识. 1.tf.name_scope() Graph中保存着一个属性_name_stack(string类型),_nam ...
- 【pytorch】pytorch学习笔记(一)
原文地址:https://pytorch.org/tutorials/beginner/deep_learning_60min_blitz.html 什么是pytorch? pytorch是一个基于p ...
- Lighthouse前端性能优化测试工具
在前端开发中,对于自己开发的app或者web page性能的好坏,一直是让前端开发很在意的话题.我们需要专业的网站测试工具,让我们知道自己的网页还有哪些需要更为优化的方面,我自己尝试了一款工具:Lig ...