【问题标题】:Finalizers accessing managed stuff终结器访问托管内容
【发布时间】:2010-07-26 17:28:26
【问题描述】:

我很清楚终结器通常用于控制非托管资源。在什么情况下终结器可以处理托管的?

我的理解是终结器队列中的存在将阻止任何对象或由此强烈引用的对象被收集,但它不会(当然)保护它们免于终结。在正常的事件过程中,一旦对象完成,它将从队列中删除,并且它引用的任何对象将不再受到保护,不会在下一次 GC 传递时被收集。在调用终结器时,可能已经为对象引用的任何对象组合调用了终结器;不能依赖以任何特定顺序调用终结器,但持有的对象引用应该仍然有效。

很明显,终结器绝不能获取锁,也不能尝试创建新对象。但是,假设我有一个订阅某些事件的对象,以及另一个实际使用这些事件的对象。如果后一个对象有资格进行垃圾回收,我希望前一个对象尽快取消订阅事件。请注意,在任何活动对象都没有订阅之前,前一个对象永远不会有资格完成。

是否有一个无锁链表堆栈或需要取消订阅的对象队列,并让主对象的终结器引用堆栈/队列上的另一个对象?必须在创建主对象时分配链表项对象(因为禁止在终结器中分配),并且可能需要使用诸如计时器事件之类的东西来轮询队列(因为事件取消订阅将不得不在终结器线程之外运行,并且拥有一个唯一目的是等待终结器队列中出现的线程可能是愚蠢的),但是如果终结器可以安全地引用其预分配的链表对象以及与其类关联的主队列对象,它可以允许事件在结束后 15 秒左右取消订阅。

这是个好主意吗? (注意:我使用的是 .net 2.0;此外,尝试添加到堆栈或队列可能会在 Threading.Interlocked.CompareExchange 上旋转几次,但我不希望它会被卡住很长时间)。

编辑

当然,任何订阅事件的代码都应该实现 iDisposable,但一次性的东西并不总是被正确处理。如果有,就不需要终结器了。

我关心的场景类似于以下内容:实现 iEnumerator(of T) 的类挂钩到其关联类的 changeNotify 事件,以便在底层类更改时可以明智地处理枚举(是的,我知道 Microsoft认为所有枚举器都应该简单地放弃,但有时可以继续工作的枚举器会更有用)。很有可能一个类的实例在几天或几周内被枚举了数千甚至数百万次,但在此期间根本没有更新。

理想情况下,枚举器在不被释放的情况下永远不会被遗忘,但枚举器有时用于“foreach”和“using”不适用的上下文中(例如,一些枚举器支持嵌套枚举)。精心设计的终结器可能会提供处理这种情况的方法。

顺便说一句,我要求任何应该通过更新继续的枚举必须使用通用的 IEnumerable(of T);如果集合被修改,不处理 iDisposable 的非泛型表单将不得不抛出异常。

【问题讨论】:

  • 你能澄清你的第三段吗?很明显,“前”对象是事件目标(订阅者),但“后”对象是事件源(引发事件的对象)吗?

标签: c# .net vb.net finalizer


【解决方案1】:

但是,假设我有一个订阅某些事件的对象,以及另一个实际使用这些事件的对象。如果后一个对象有资格进行垃圾回收,我希望前一个对象尽快取消订阅事件。请注意,前一个对象永远不会有资格进行最终确定,直到任何活动对象都没有订阅它。

如果“后者”对象是使用事件的对象,而“前者”对象是订阅事件的对象,则“前者”对象必须有某种方式将事件信息传递给“后者” " 对象 - 意味着它将对“后者”有一些参考。很有可能,这将阻止“后”对象永远成为 GC 候选对象。


话虽如此,除非绝对必要,否则我建议避免通过终结器进行这种类型的托管资源释放。您所描述的架构似乎非常脆弱,并且很难正确处理。这可能是 IDisposable 的更好候选者,终结器是“最后的”清理工作。

虽然IDisposable 通常是关于释放本机资源 - 它可以是关于释放任何资源,包括您的订阅信息。

另外,我会尽量避免使用单一的全局对象引用集合——让您的对象在内部使用WeakReference 可能更有意义。一旦收集到“后者”对象,“前者”对象的 WeakReference 将不再有效。下次引发事件订阅时,如果内部 WeakReference 不再有效,您可以自行取消订阅。不需要全局队列、列表等 - 它应该可以工作......

【讨论】:

  • 前一个对象对后一个对象的引用必须是一个弱引用。当然,对象应该实现 iDisposable,但终结器的全部意义在于处理应该调用但没有调用 iDisposable 的情况。在订阅事件触发时修复问题是一种常见的方法,也是一种很好的方法,但如果有问题的事件类似于 Disposed,它可能不会在任意长时间内发生。
  • @supercat:“......如果有问题的事件类似于 Disposed,它可能不会在任意长时间内发生。”如果唯一的目标是取消订阅该事件,这并不重要。它可以花你想要的时间,因为在事件被触发之前它什么都不做......
  • 如果持有订阅的对象会长期存在,那么订阅本质上就是内存泄漏。在大多数情况下,它可能足够小,无关紧要,但最好完全消除内存泄漏。如果 queue-item 对象包含一个 StringBuilder,则清理最终对象的计时器事件可以记录其值,帮助追踪泄漏。
【解决方案2】:

我将调用对象“发布者”和“订阅者”并重申我对问题的理解:

在 C# 中,发布者将(有效地)持有对订阅者的引用,防止订阅者被垃圾回收。我该怎么做才能在不显式管理订阅的情况下对订阅者对象进行垃圾回收?

首先,我建议您尽我所能首先避免这种情况。现在,我将继续并假设你有,考虑到你仍然在发布这个问题 =)

接下来,我建议挂钩发布者事件的添加和删除访问器,并使用 Wea​​kReferences 集合。然后,您可以在调用事件时自动取消挂钩这些订阅。这是一个非常粗略、未经测试的示例:

private List<WeakReference> _eventRefs = new List<WeakReference>();

public event EventHandler SomeEvent
{
    add
    {
        _eventRefs.Add(new WeakReference(value));
    }
    remove
    {
        for (int i = 0; i < _eventRefs; i++)
        {
            var wRef = _eventRefs[i];
            if (!wRef.IsAlive)
            {
                _eventRefs.RemoveAt(i);
                i--;
                continue;
            }

            var handler = wRef.Target as EventHandler;
            if (object.ReferenceEquals(handler, value))
            {
                _eventRefs.RemoveAt(i);
                i--;
                continue;
            }
        }
    }
}

【讨论】:

  • 那个方法实际上行不通(我很久以前研究过)。问题是唯一能抓住代表的是一个弱引用!您可以通过运行该代码来测试它,也可以调用 GC.Collect() 来强制收集。事件处理程序将停止运行,因为委托已被收集。
  • 顺便说一句:我同意尽一切可能避免这种情况。
【解决方案3】:

让我确保我理解 - 您是否担心仍然订阅收集的事件发布者的事件订阅者的泄漏?

如果是这样,那我认为你不必担心。

我的意思是假设“前”对象是事件订阅者,“后”对象是事件发布者(引发事件):

订阅者(前者)被“订阅”的唯一原因是您创建了一个委托对象并将该委托传递给发布者(“后者”)。

如果您查看委托成员,它具有对订阅者对象和订阅者上将被执行的方法的引用。所以有一个参考链看起来像这样:发布者 --> 委托 --> 订阅者(发布者引用委托,它引用订阅者)。这是一个单向链——订阅者不持有对委托的引用。

因此,保留委托的唯一根是发布者(“后者”)。当后者有资格获得 GC 时,委托也有资格。除非您希望订阅者在取消订阅时采取一些特殊操作,否则在收集委托时他们将有效地取消订阅 - 没有泄漏)。

编辑

根据 supercat 的 cmets,听起来问题在于发布者让订阅者保持活动状态。

如果这是问题所在,那么终结器将无济于事。原因:您的发布者(通过委托)对您的订阅者有一个真实的、真正的引用,并且发布者是根的(否则它将有资格获得 GC),因此您的订阅者是根的,并且没有资格获得最终确定或 GC。

如果您在发布者保持订阅者活动时遇到问题,我建议您搜索弱引用事件。这里有几个链接可以帮助您入门:http://www.codeproject.com/KB/cs/WeakEvents.aspxhttp://www.codeproject.com/KB/architecture/observable_property_patte.aspx

我也不得不处理一次。大多数有效的模式都涉及更改发布者,以便它持有对委托的弱引用。然后你有一个新问题 - 代表没有根,你必须以某种方式让它保持活力。上面的文章可能会做类似的事情。一些技术使用反射。

我曾经使用过一种不依赖于反思的技术。但是,它要求您能够更改发布者和订阅者中的代码。如果您想查看该解决方案的示例,请告诉我。

【讨论】:

  • 如果发布者超出范围,所有代表都会再见,一切都很好。如果 SUBSCRIBER 有效地超出范围但发布者将在范围内停留很长时间,则会出现问题。
  • 所以您遇到了发布者保持订阅者存活的问题? (无意冒犯,但很难在您的原始帖子中了解谁是发布者,谁是订阅者)。我在这里发表了评论,但我要编辑我的帖子——这样可以有更多空间。
  • 顺便说一句,我看过 CodeProject 上的“弱事件”文章,并不真正喜欢任何解决方案;回顾这篇文章,似乎确实提到了使用终结器的可能性,但是直接从终结器中取消注册似乎非常危险。将注销放入堆栈或队列中,以便由非终结器线程完成似乎更安全。
  • @JMarsch:我提出了一个使用弱引用和通用接口的类事件系统。调用 raiseAction(of T) 将对所有订阅者调用 iDoAction(of T)。也许我过于努力地提出了一个通用的弱委托概念,但是将 finalize-notifiers 放在预分配队列中的方法似乎也有其他原因,例如记录对象的事实没有被处理就已经完成了。
【解决方案4】:

让我们再试一次。您可以像这样将您的事件处理程序添加到您的发布者:

var pub = new Publisher();
var sub = new Subscriber();
var ref = new WeakReference(sub);

EventHandler handler = null; // gotta do this for self-referencing anonymous delegate

handler = (o,e) =>
{
    if(!ref.IsAlive)
    {
        pub.SomeEvent -= handler; // note the self-reference here, see comment above
        return;
    }


    ((Subscriber)ref.Target).DoHandleEvent();
};

pub.SomeEvent += handler;

这样,您的代理不会保留对订阅者的直接引用,并且会在订阅者被收集时自动取消挂钩。您可以将其实现为 Subscriber 类的私有静态成员(出于封装目的),只需确保它是静态的,以防止无意中持有对“this”对象的直接引用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-08-01
    • 2019-02-26
    • 2023-03-24
    • 2020-08-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多