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. … Continue reading →

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:

Hide   Copy Code

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:

Hide   Copy Code

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:

Hide   Copy Code

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:

Hide   Copy Code

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:

Hide   Copy Code

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 WeakEventManager base 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 true if 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:

Hide   Shrink    Copy Code

public 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:

Hide   Copy Code

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:

Hide   Copy Code

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:

Hide   Copy Code

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:

Hide   Copy Code

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:

Hide   Copy Code

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:

Hide   Copy Code

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.

时间: 2024-08-24 07:42:14

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 that handlers that are attached to event sources will not be destroyed in coordination with the listener object that attached the handler to the source.

Weak Event Manager

问题 通过传统的方式监听事件(即C#的+=语法),有可能会导致内存泄漏,原因是事件源会持有对事件Handler所在对象的强引用从而阻碍GC回收它,这样事件handler对象的生命周期受到了事件源对象的影响. 解决方案 此问题有两个解决办法:1) 确保通过-=语法反注册事件处理器 2)使用弱事件模式(Weak Event Pattern).本文主要讲解Weak Event Pattern. 在使用Weak Event Pattern时,主要涉及到两个类:WeakEventManager和IWeak

WPF: 深入理解 Weak Event 模型

在之前写的一篇文章(XAML: 自定义控件中事件处理的最佳实践)中,我们曾提到了在 .NET 中如果事件没有反注册,将会引起内存泄露.这主要是因为当事件源会对事件监听者产生一个强引用,导致事件监听者无法被垃圾回收. 在这篇文章中,我们首先将进一步说明内存泄露的问题:然后,我们会重点介绍 .NET 中的 Weak Event 模型以及它的应用:之所以使用 Weak Event 模型就是为了解决常规事件中所引起的内存泄露:最后,我们会自己来实现 Weak Event 模型. 一.再谈内存泄露 1.

C#中的弱事件(Weak Events in C#)

(原创翻译文章·转载请注明来源:http://blog.csdn.net/hulihui) 原文:Weak Events In C#: Different approaches to weak events. by Daniel Grunwald.  Download source code - 15.5 KB 翻译前序 翻译后记 目录 引言 究竟什么是事件? 第1部分:监听方(Listener-side)的弱事件 解决方案0:仅仅注销 解决方案1:事件调用后注销 解决方案2:带弱引用(Weak

C#学习日记24----事件(event)

事件为类和类的实例提供了向外界发送通知的能力,实现了对象与对象之间的通信,如果定义了一个事件成员,表示该类型具有 1.能够在事件中注册方法 (+=操作符实现). 2.能够在事件中注销方法(-=操作符实现). 3.当事件被触发时注册的方法会被通知(事件内部维护了一个注册方法列表).委托(Delegate)是事件(event)的载体,要定义事件就的要有委托.  有关委托的内容请点击 委托(De... www.mafengwo.cn/event/event.php?iid=4971258www.maf

Data Binding和INotifyPropertyChanged是如何协调工作的?

前言 WPF的一大基础就是Data Binding.在基于MVVM架构的基础上,只有通过实现INotifyPropertyChanged接口的ViewModel才能够用于Data Binding. 要实现INotifyPropertyChanged接口,只需要实现一个事件,event PropertyChangedEventHandler PropertyChange. delegate & event基础知识回顾 先来回顾下C#里delegate和event的基础知识. 我们知道在C#里,ev

Logstash 父子关系 配置

最近在使用Lostash的过程中遇到了一个问题:在一个log文件里包含两类数据,而且两类数据之间存在父子关系,那如何使用lostash的configuration实现这个需求呢 思路: 首先定义父事件的pattern,因为子事件不匹配父pattern,所以logstash会自动为子事件添加_grokparesefailure 标签.通过该标签即可知道当前事件是父事件还是子事件 使用filter->ruby生成document_id,并把它放到ruby全局变量中 ,这样子事件就可以访问到父事件的d

C#编程实践—EventBroker简单实现

前言 话说EventBroker这玩意已经不是什么新鲜货了,记得第一次接触这玩意是在进第二家公司的时候,公司产品基础架构层中集成了分布式消息中间件,在.net基础服务层中使用EventBroker的模式将消息组装成事件,支持同域.跨域和跨机器进行事件的发布和订阅,后来才知道这玩意叫做EventBroker.不得不承认,这是一个非常聪明的东西,它在内部高度封装了消息和事件的处理,将上层应用的事件和委托的依赖进行解耦,并且提供非常简洁的方式进行开发.OK,本篇文章只是实现了一个简化版本的EventB

Esper 20章 优化

20 优化esper为了处理高速的生成力已经高度优化,并且接收事件和输出结果低延迟.esper还可以进一步最大化可测使用在 软实时和硬实时JVM 上. 本章描述了最好的优化练习,而且解释了怎么去评价esper优化 通过使用我们提供的优化工具 20.1 优化结果 为了进一步理解以下测试结果,请查询下一章 测试结果说明: eseper在一个双核2GH Ineter硬件,处理速度超过 500000 event/s,并且引擎延迟平均低于3微秒(超过99%的测试低于10us(微秒)) 在一个平均加权基准点