다음을 통해 공유


Simulating “Weak Delegates” in the CLR

Introduction

What follows may seem like a fairly obscure topic relating to lifetime management in the CLR, but for those who have hit upon this scenario (and they may not even know they have), this is a very helpful pattern to know about.

In some of the development of Avalon, we hit upon a pattern for associating event handlers from a generic notion of Containee to a generic notion of Container. This results in strong CLR references to the Container, and can thus prevent garbage collection of the Container so long as the Containee is alive. This is similar to the problem that WeakReferences in the .NET Framework were designed to solve, but somewhat different in that this scenario is specific to events.

This becomes a real issue if:

a) the Containee outlives the Container (or at least outlives the intended lifetime of the Container), and,

b) the Container is substantially more heavyweight than the Containee, and thus results in substantial resources being held onto

This certainly doesn’t happen all the time, but when it does, one needs a good pattern and solution for dealing with it. The rest of this post lays out such a pattern and solution.

The Problem

The specific instance of this problem we’re facing in the Avalon is that there’s a key class in the system called a Visual. A Visual contains a Transform (for 2D transformation). That Transform need not be used only in one place, and may be contained by a bunch of other Visuals (and other objects) as well. Furthermore, the Transform can change, and when it changes, the Visual (or Visuals) needs to be notified. From the programmer’s point of view, there’s no reference from the Transform to the Visual that contains it, since the programmer simply thinks of assigning a Transform into a Visual, not vice-versa. Therefore, there’s no reason to expect that if I hang on to a Transform, that I’m keeping the Visuals alive that reference that Transform. However, that’s exactly the case, since the Visuals are listeners on the events published by the Transform.

This is simply a specific case of a much more general problem of Containers and Containees, where the Containee has a hidden reference (from the point of view of the programmer) to the Container, and thus forces the Container to stay alive. Here’s the situation I’m talking about:

Furthermore, since the Containee can be used in multiple places, we get these graphs:

And this is even more insidious than the previous one since having one of the Containers still be alive forces the other one to stay alive. This gets worse the more places the Containee is used.

A Solution

An ideal solution to this would be some explicit CLR notion of “Weak Delegates”. That is, a delegate that can be used to attach to an event whose target is inherently a weak reference. The CLR doesn’t currently have such a feature, so we must make do with what we have. Thus, given that there is no such first-class notion of a “weak delegate”, we can employ a pattern to get a very good approximation of the problem we’re trying to solve.

The approach is to have the Containee register for an event not on the Container, but on a class that derives from WeakReference and implements an event with the same signature as that on the Container. It then holds a reference to the container as the WeakReference’s “target”. This class, in the code below, is called a WeakContainer, and approximates what a “Weak Delegate” would really do. When it comes time to raise the event on the Containee, the following occurs:

  • Containee just fires its events, and since the WeakContainer is a target for one of these, its handler runs.
  • That handler first checks to see if Target (from WeakReference) is not null (meaning the target object is still alive)
    • If it is, it simply calls through that targets handler.
    • If it isn’t, then it removes itself from the sender’s handler list, allowing the WeakContainer itself to be collected.

This approach results in the following for the “single use of Containee” case, as in the first diagram above:

And the multiple use of Containee looks like:

Note that:

  • See code for all of this at the end of this post.
  • The Containee knows nothing about any of this. The Container is responsible for setting up the “weak delegate” construct.
  • The WeakContainer class is a private nested class inside the Container class. No need for anyone outside to know about it.
  • There’s likely room for generalizing this for multiple event signatures through the use of generics. I didn’t want to confuse the issue with that for now.
  • The WeakContainer class (to which Containee has a strong reference) may stick around indefinitely, as they are only reaped when an event is raised after their target has been collected. That is one downside here, but note that only the WeakContainer class itself (which is basically just a shell) is temporarily leaked. Far, far better than leaking the Container itself.
  • There are a few other subtleties here, but the above is the basic idea. See the code below for the exact usage.

Code

using System;

using System.Diagnostics;

namespace WeakDelegate

{

// Containee needn't know anything about weak references, etc.

// It just fires its events as normal.

class Containee

{

public Containee(string id) { _id = id; }

public event EventHandler MyEvent;

public void DoIt()

{

if (MyEvent != null)

{

MyEvent(this, new EventArgs());

}

}

public string ID { get { return _id; } }

~Containee()

{

Console.WriteLine(" Containee '{0}' cleaned up", ID);

}

private string _id;

}

// Container has a reference to Containee. But doesn't want to

// force Containee to have a strong reference to it. Uses a

// WeakContainer private nested class for this.

class Container

{

public void Handler1(object sender, EventArgs args)

{

Console.WriteLine(

" Container.Handler1 called with Containee '{0}'",

_containee == null ? "<none>" : _containee.ID);

}

public Containee Containee

{

get

{

return _containee;

}

set

{

if (_weakContainer == null)

{

_weakContainer = new WeakContainer(this);

}

// unsubscribe old

if (_containee != null)

{

_containee.MyEvent -= new

EventHandler(_weakContainer.Handler1);

}

// subscribe new

_containee = value;

if (_containee != null)

{

_containee.MyEvent += new

EventHandler(_weakContainer.Handler1);

}

}

}

~Container()

{

Console.WriteLine(" Container cleaned up");

}

#region WeakContainer

private class WeakContainer : WeakReference

{

public WeakContainer(Container target) : base(target) {}

public void Handler1(object sender, EventArgs args)

{

Console.WriteLine(" WeakContainer.Handler1 called");

Container b = (Container)this.Target;

if (b != null)

{

b.Handler1(sender, args);

}

else

{

Containee c = sender as Containee;

if (c != null)

{

c.MyEvent -= new EventHandler(this.Handler1);

Console.WriteLine(

" Removed WeakContainer handler");

}

}

}

~WeakContainer()

{

Console.WriteLine(" WeakContainer cleaned up");

}

}

#endregion

private Containee _containee;

private WeakContainer _weakContainer = null;

}

class Class1

{

/// <summary>

/// The main entry point for the application.

/// </summary>

[STAThread]

static void Main(string[] args)

{

(new Class1()).Run();

}

private Containee containee1, containee2;

private void InvokeMe()

{

Console.WriteLine("Invoking");

if (containee1 != null) containee1.DoIt();

if (containee2 != null) containee2.DoIt();

}

public void Run()

{

containee1 = new Containee("FIRST");

containee2 = new Containee("SECOND");

Container container = new Container();

container.Containee = containee1;

InvokeMe();

Console.WriteLine("Switching to containee2");

container.Containee = containee2;

InvokeMe();

Console.WriteLine("Switching back to containee1");

container.Containee = containee1;

InvokeMe();

Console.WriteLine("Setting container to null and GC'ing");

container = null;

GC.Collect();

GC.WaitForPendingFinalizers();

InvokeMe();

InvokeMe();

Console.ReadLine();

}

}

}

Finally, note that in this particular chunk of code, the diagrams above aren’t quite complete. The missing piece is that Container maintains a strong reference to WeakContainer. But that’s fine, it doesn’t prevent Container from going away just because Containee is still alive. Here’s the more accurate diagram:

Comments

  • Anonymous
    May 27, 2004
    This looks like an interesting article. I must be misunderstanding it on initial glance, because it reminds me of horrible COM circular reference stuff that presumably couldnt happen in .NET.
    So I'll have to come back to it later for a proper look!

  • Anonymous
    June 02, 2004
    Great article, Greg. I was myself the "victim" of strong references made by long living event publishers. I wonder why events/handlers were not implemented as weak delegates by default - event publishers should not depend on its subscribers and the way subscribers handle the event. Although the pattern you're suggesting is an improvement, it is still not a complete solution. The container still responds to events even after being selected for collection by garbage collector, when weak reference becomes the only reference to a container. Target property of WeakReference becomes null only after GC collected the object. That's why you nulled it and immediately called GC.Collect() in your test application. I don't know is it only me, but I expect the object that is selected for garbage collection but not collected yet, to be "dead" and not to respond to events it's subscribed to. Weak references do not solve this problem, but at least, comparing to strong references, they allow for garbage collector to collect container object (event subscribers) when the time comes.

  • Anonymous
    June 03, 2004
    For this particular problem, it would have been far simpler for the Transform, or better yet, a container for the Transform, to have a reference to the parent, and let that trickle up the tree.

    Here is how we do it in VG.net:
    - Each Element (graphical object) can optionally contain a transformation
    - The transformation is exposed via properties,
    such as Rotation
    - If you set a transforamtion property, internally a Transfomation struct is stored, and
    - Every time a transformation property is modified, a child element notifies it's parent, and so on, on up the tree.

    What is wrong with this simple approach?

  • Anonymous
    June 03, 2004
    Predrag... you're right in saying that the weak reference's Target may not yet be null even though the Container no longer has any strong references to it.

    However, the reason I null'd it and called GC.Collect() in the test application was merely to test make it clear that the approach was working. I don't expect this to be done in a real usage.

    The upshot is that the container needs to be resilient in the face of receiving event handler invocations even if there were no outstanding strong references to it. Typically, I don't think this should be a problem.

    But, fundamentally, you're definitely right that this isn't the end-all, be-all solution. An approach that had a first-class notion of "weak delegates" would likely be the way to go.

  • Anonymous
    June 03, 2004
    The comment has been removed

  • Anonymous
    June 06, 2004
    variation on the themea and alternate solution

  • Anonymous
    June 23, 2004
    The comment has been removed

  • Anonymous
    April 11, 2005
    Well here we are again .&amp;nbsp; For those of you who remember, I made a post a while back about events...

  • Anonymous
    February 16, 2006
    What follows may seem like a fairly obscure topic relating to lifetime management in the CLR, but for those who have hit upon this scenario (and they may not even know they have), this is a very helpful pattern to know about.

    In some of the development

  • Anonymous
    October 02, 2006
    Interesting post over here from Greg Schechter about the idea of a "weak delegate". The CLR has support...

  • Anonymous
    December 14, 2006
    PingBack from http://bryanallott.net/journal/2006/11/07/delegate-gotcha/

  • Anonymous
    January 09, 2007
    This problem actually comes up pretty often so I thought I'd write a little article about it, and a couple

  • Anonymous
    January 11, 2007
    The comment has been removed

  • Anonymous
    February 12, 2007
    You've been kicked (a good thing) - Trackback from DotNetKicks.com

  • Anonymous
    February 15, 2007
    PingBack from http://www.sral.org/2007/02/16/links-for-2007-02-16/

  • Anonymous
    March 02, 2007
    PingBack from http://www.wesay.org/blogs/2006/08/19/34/

  • Anonymous
    April 22, 2007
    Há uns tempos atrás o colega Miguel Carvalho chamou-me a atenção para um bug que tinha encontrado num

  • Anonymous
    September 17, 2007
    The comment has been removed

  • Anonymous
    October 11, 2007
    PingBack from http://www.paulstovell.net/blog/index.php/wpf-binding-bug-leads-to-possible-memory-issues/

  • Anonymous
    February 04, 2008
    &#160; There are numbers of blogs that folks wrote about memory leaks in Microsoft .Net Framework managed

  • Anonymous
    February 04, 2008
    There are numbers of blogs that folks wrote about memory leaks in Microsoft .Net Framework managed code

  • Anonymous
    February 04, 2008
    PingBack from http://msdnrss.thecoderblogs.com/2008/02/04/finding-memory-leaks-in-wpf-based-applications/

  • Anonymous
    February 13, 2008
    PingBack from http://robburke.net/2008/02/13/are-your-clr-based-apps-leaking/

  • Anonymous
    March 26, 2008
    Event handling subscribe / unsubscribe to an event

  • Anonymous
    February 10, 2009
    Memory leaks are always headache of developers. Do .NET developers no longer bother to worry about memory

  • Anonymous
    March 09, 2009
    One of the nice things about developing on a platform that uses a garbage collecting memory manager (like

  • Anonymous
    March 21, 2009
    It's been a while since the last post was online. We have been very busy in working on one of the very

  • Anonymous
    April 06, 2009
    PingBack from http://joshsmithonwpf.wordpress.com/2009/04/06/a-mediator-prototype-for-wpf-apps/

  • Anonymous
    May 31, 2009
    PingBack from http://indoorgrillsrecipes.info/story.php?id=4371

  • Anonymous
    June 07, 2009
    PingBack from http://greenteafatburner.info/story.php?id=3126