Phase 11 of 30 · Topic 11.2

The Classic Event-Subscriber Memory Leak

1Concept

Subscribing to an event on a long-lived publisher creates a strong reference from the publisher to the subscriber. The subscriber cannot be garbage collected, creating silent memory leaks.

2Architecture Diagram

[ Long-Lived Singleton Publisher ]
                 │ (Holds strong reference in delegate invocation list)
                 ▼
[ Short-Lived Transient Form / Service ] ──> Cannot be collected by GC!

3Code Example

C# 13 & .NET 9
using System;

public class LongLivedPublisher
{
    public event Action? GlobalTick;
    public void Trigger() => GlobalTick?.Invoke();
}

public class ShortLivedSubscriber : IDisposable
{
    private readonly LongLivedPublisher _publisher;
    private readonly string _name;

    public ShortLivedSubscriber(LongLivedPublisher publisher, string name)
    {
        _publisher = publisher;
        _name = name;
        _publisher.GlobalTick += OnTick; // Strong reference created!
    }

    private void OnTick() => Console.WriteLine($"[{_name}] Tick received.");

    public void Dispose()
    {
        _publisher.GlobalTick -= OnTick; // Crucial: Breaks reference!
        Console.WriteLine($"[{_name}] Unsubscribed cleanly.");
    }
}

public class MemoryLeakDemo
{
    public static void Main()
    {
        var pub = new LongLivedPublisher();
        using (var sub = new ShortLivedSubscriber(pub, "TempWorker"))
        {
            pub.Trigger();
        }
        Console.WriteLine("Worker disposed and unhooked.");
    }
}

4Expected Output

[TempWorker] Tick received.
[TempWorker] Unsubscribed cleanly.
Worker disposed and unhooked.

5Key Takeaways

  • Always unsubscribe events in `Dispose()`.
  • Long-lived publishers are the #1 source of memory leaks in desktop and server apps.
  • Use memory profilers (dotMemory / PerfView) to inspect GC roots.