为什么事件处理程序会阻止垃圾回收的发生

9

我有这段代码

public class Publisher
{
    public event EventHandler SomeEvent;
}

public class Subscriber
{
    public static int Count;

    public Subscriber(Publisher publisher)
    {
        publisher.SomeEvent += new EventHandler(publisher_SomeEvent);
    }

    ~Subscriber()
    {
        Subscriber.Count++;
    }

    private void publisher_SomeEvent(object sender, EventArgs e)
    {
        // TODO
    }
}

在我的应用程序的Main方法中,我有
static void Main(string[] args)
{
    Publisher publisher = new Publisher();

    for (int i = 0; i < 10; i++)
    {
        Subscriber subscriber = new Subscriber(publisher);
        subscriber = null;
    }

    GC.Collect();
    GC.WaitForPendingFinalizers();

    Console.WriteLine(Subscriber.Count.ToString());
}

如果我运行这个程序,输出将会是0。 如果我从代码中移除事件订阅,我将得到期望的结果——也就是10。
当调用GC.Collect()时,垃圾回收被强制启动。因为Subscriber在其中定义了Finalize,所以GC将暂停收集,直到finalizequeue为空,也就是所有Subscription实例都将调用其Finalize()方法之后(如果我的假设不正确,请纠正我)。在下一行调用了GC.WaitForPendingFinalizers(),这将有效地挂起执行,直到终结器队列为空。现在,因为我们的输出为0,我相信Finalize()没有被调用,这使我相信GC没有标记订阅者实例要被收集,因此Finalizer()方法没有被调用。
所以我有两个问题:
1.我的假设是否正确,事件订阅是否阻止GC标记订阅者实例进行收集? 2.如果是这样,那是因为发布者持有对订阅者的引用吗?(垃圾收集器和事件处理程序)
我的猜想是,由于有 10 个 Subscriber 实例引用同一个 publisher 实例,当进行 GC 回收时,它会看到有其他对 publisher 的引用,因此无法回收,结果订阅实例和发布者一起被移动到下一代,因此垃圾回收不会发生,也不会在代码执行到 Console.WriteLine(Subscriber.Count.ToString()) 时调用 Finalize()。在这里我对吗?

1
这样想吧:当调用Publisher.SomeEvent委托时,它将执行当前订阅的任何内容。这意味着在订阅处于活动状态时,订阅者实例必须保持活动状态。这就是为什么发布者必须保留所有订阅者的引用。由于订阅者是可达的,GC无法收集它们。 - Andrew Savinykh
2个回答

4
你误认为发生了什么事情,这是C#中非常普遍的陷阱。你需要运行测试程序的发布版本并在没有调试器的情况下运行它(按Ctrl + F5)。这样它就会在用户的机器上运行。现在请注意,无论您是否订阅事件,您都将始终获得10,已经完全不再重要了。
问题在于,当使用调试器时,发布者对象不会被收集。我在这个答案中详细解释了原因。
再进一步解释一下,这里有一个循环引用。订阅者对象引用发布者对象。而发布者对象也引用订阅者对象。循环引用不足以保持对象存活。庆幸的是,如果那样的话垃圾回收将不会很有效。发布者对象必须在别处被引用才能保持存活,局部变量不够好。

谢谢你的回答!确实,当我通过按下Ctrl + F5来运行发布版本时,输出为10。 - Michael
"订阅者对象引用发布者对象。真的吗?" - Henrik
我猜这意味着只要发布者对象继续被引用,订阅者对象就基本上能够保持活跃? - Panzercrisis
当出版者超越订阅者的生命周期时,这是不健康的。这是WPF中的一个大问题,他们提出了“弱事件模式”。这不是一个通用的解决方案,它会消耗很多的周期来发现已经死亡的订阅者。对于UI相关的事件来说还好,因为UI运行在人类的速度下,即毫秒级别。程序员常常会使用IDisposable来取消事件订阅,但这也是不正确的。 - Hans Passant

3
这两个问题的答案都是肯定的。

谢谢确认,您认为最后一段的假设也是正确的吗? - Michael
@michaelmoore 订阅者不持有对发布者的引用。是在 Main 中的局部变量 publisher 阻止了 Publisher 实例以及其中的 10 个 Subscriber 实例被回收。 - Henrik
2
你没有解释为什么 publisher 没有被收集。它确实被收集了,只是当 OP 尝试运行他的代码时没有被收集。 - Hans Passant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接