终结器访问托管内容

4
我知道finalizers通常用于控制非托管资源。在什么情况下,finalizer 可以处理托管资源呢?
我的理解是,存在于 finalizer 队列中将防止任何对象或被强引用的对象被回收,但它当然不能保护它们免受最终化。在正常情况下,一旦对象完成最终化,它将从队列中删除,并且它所引用的任何对象将不再受到保护,可以在下一个 GC 通过时进行回收。在调用 finalizer 时,finalizer 可能已经针对对象引用的任何组合进行了调用;不能指望 finalizer 按任何特定顺序进行调用,但是您持有的对象引用应该仍然有效。
很明显,finalizer 绝不能获取锁,也不能尝试创建新对象。但是,假设我有一个订阅某些事件的对象和另一个实际使用这些事件的对象。如果后者的垃圾收集变得可行,我希望前者的对象在尽可能短的时间内取消订阅事件。请注意,前者的对象直到没有任何活动对象持有其订阅时才会变得不适合最终化。
是否可以拥有一个无锁链接列表堆栈或队列,其中包含需要取消订阅的对象,并且主对象的 finalizer 将对其他对象的引用放在堆栈/队列上?链接列表项对象必须在创建主对象时分配(因为在 finalizer 中分配将被禁止),并且可能需要使用诸如计时器事件之类的东西来轮询队列(因为事件取消订阅必须在 finalizer 线程之外运行,而且将一个线程作为等待 finalizer 队列中出现某些内容的唯一目的可能很愚蠢),但是如果 finalizer 可以安全地引用其预分配的链接列表对象和与其类相关联的主队列对象,则可以允许在最终化后的大约 15 秒内取消订阅事件。
这样做是否明智?(注:我正在使用 .net 2.0;此外,尝试添加到堆栈或队列可能会在 Threading.Interlocked.CompareExchange 上旋转几次,但我不希望它被卡住太久)。
编辑
当然,任何订阅事件的代码都应该实现 IDisposable,但是可处置的东西并不总是正确处置。如果是这样,就不需要 finalizers 了。
我担心的情况是以下情况之一:实现 iEnumerator(of T) 的类钩子挂接到其关联类的 changeNotify 事件上,以便在底层类更改时可以合理地处理枚举(是的,我知道 Microsoft 认为所有枚举器都应该放弃,但有时一个可以继续工作的枚举器将更有用)。这个类的实例可能在数天或数周内被枚举数千甚至数百万次,但在此期间不会进行更新。
理想情况下,枚举器不会被遗忘而没有被处理,但有时在一些不适用于“foreach”和“using”的情况下使用枚举器(例如,某些枚举器支持嵌套枚举)。一个经过精心设计的终结器可能允许处理这种情况。
顺便提一下,我要求任何需要在更新中继续的枚举都必须使用泛型IEnumerable(of T);不支持iDisposable的非泛型形式如果集合被修改,则必须抛出异常。

1
你能澄清一下你的第三段吗?"前者"对象是事件目标(订阅者)非常清楚,但"后者"对象是事件源(引发事件的对象)吗? - Brian Gideon
4个回答

3
假设我有一个对象订阅了某些事件,另一个对象实际上使用这些事件。如果后者的垃圾回收变得可行,我希望前者能够尽快取消订阅事件。请注意,在任何存活对象不持有它的订阅之前,前者永远不会变得适合进行终结处理。
如果“后者”是使用事件的对象,“前者”是订阅事件的对象,那么“前者”必须有一种方式将事件信息传递给“后者” - 这意味着它将在“后者”中保留一些引用。很可能,这将使“后者”对象永远不会成为GC候选。
那么,我建议避免使用终结器来管理资源释放,除非绝对必要。您描述的架构似乎非常脆弱,很难正确处理。这可能更适合使用IDisposable,其中终结器是“最后一道防线”的清理尝试。
虽然 IDisposable通常用于释放本机资源,但它可以用于释放任何资源,包括您的订阅信息。
此外,我建议避免使用单个全局对象引用集合 - 在内部使用 WeakReference可能更有意义。一旦“后者”对象被收集,那么“前者”对象的WeakReference将不再有效。下次引发事件订阅时,如果内部WeakReference不再有效,则可以取消订阅自己。无需全局队列、列表等 - 它应该只需要工作...

前一个对象对后一个对象的引用必须是WeakReference。当然,这些对象应该实现iDisposable,但finalizers的整个目的是处理应该调用iDisposable但未调用的情况。在订阅事件触发时修复问题是一种常见且好的方法,但如果涉及的事件是Disposed之类的事件,则可能需要等待任意长的时间才能发生。 - supercat
@supercat:“……如果涉及的事件是Disposed之类的,它可能会在任意长的时间内不发生。” 如果唯一的目标是取消订阅事件,那么这并不重要。它可以花费任意长的时间,因为它无论如何都不会执行任何操作,直到事件被触发…… - Reed Copsey
如果持有订阅的对象存在很长时间,那么该订阅本质上就是一个内存泄漏。在大多数情况下,这可能只是一个微不足道的小问题,但最好完全消除内存泄漏。如果队列项对象持有一个 StringBuilder,则清理已完成对象的计时器事件可以记录其值,以帮助跟踪泄漏。 - supercat

0

我将称这些对象为“发布者”和“订阅者”,并重新阐述我的问题理解:

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

首先,我建议尽一切可能避免出现这种情况。现在,我将继续并假设您已经考虑过这一点,因为您仍然发布了这个问题 =)

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

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()来强制进行垃圾回收。事件处理程序将停止运行,因为委托已被回收。 - JMarsch
顺便说一句:我同意尽一切可能避免这种情况。 - JMarsch

0

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

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”对象的直接引用。

0

让我确认一下,您是否担心事件订阅者从已收集的事件发布者那里泄漏?

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

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

订阅者(前者)之所以“订阅”是因为您创建了一个委托对象,并将该委托传递给了发布者(“后者”)。

如果您查看委托成员,则其具有对订阅者对象和要执行的订阅者方法的引用。因此,存在如下引用链:发布者 -> 委托 -> 订阅者(发布者引用委托,委托引用订阅者)。这是一个单向链 - 订阅者不持有对委托的引用。

因此,唯一使委托保留的根源在于发布者(“后者”)。当后者变得符合垃圾回收条件时,委托也会被回收。除非您希望订阅者在取消订阅时执行某些特殊操作,否则当委托被收集时,他们将有效地取消订阅 - 没有泄漏。

编辑

根据supercat的评论,问题似乎是发布者一直在保持订阅者的活动状态。

如果是这个问题,那么终结器是无法帮助你的。原因是:您的发布者通过委托拥有对订阅者的真正、合法引用,并且发布者是根(否则它将有资格进行GC),因此您的订阅者是根,并且不会有资格进行终结或GC。

如果您在处理发布者一直保持订阅者活动状态的问题,则建议您搜索弱引用事件。以下是一些链接,可供您开始使用:http://www.codeproject.com/KB/cs/WeakEvents.aspx http://www.codeproject.com/KB/architecture/observable_property_patte.aspx

我也曾经遇到过这个问题。大多数有效的模式都涉及更改发布者,使其持有委托的弱引用。然后,您就会面临一个新问题——委托没有根,您必须以某种方式保持其活动状态。上述文章可能会采用类似的方法。有些技术使用反射。

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


如果发布者超出范围,所有委托就会消失,一切都没问题。但是,如果订阅者有效地超出范围,但发布者将长时间保持在范围内,问题就会出现。 - supercat
所以你遇到了发布者一直保持订阅者存活的问题?(不冒犯,但是在你原始帖子中很难跟踪谁是发布者和谁是订阅者)。我曾在这里发表评论,但现在我要编辑我的帖子——这样更方便。 - JMarsch
也许我需要在原帖中添加另一个编辑,以明确持有订阅的对象将持有对订阅感兴趣的任何人所持有的对象的弱引用。 - supercat
顺便说一下,我在CodeProject上看到了关于"弱事件"的文章,但并不喜欢其中任何解决方案;回头再看这篇文章时,似乎提到了使用终结器的可能性,但是直接在终结器内部取消注册似乎非常危险。将取消注册放入堆栈或队列中,以便由非终结器线程完成似乎更安全。 - supercat
@JMarsch:我使用弱引用和通用接口设计了一种类似事件的系统。调用raiseAction(of T)将在所有订阅者上调用iDoAction(of T)。也许我正在努力实现一个通用的弱委托概念,但是将finalize-notifiers放入预分配的队列中的方法似乎也有其他好处,例如记录对象已经完成但未被处理的事实。 - supercat
@supercat,我的弱事件解决方案与您描述的非常相似。稍加调整,实际上可以使用原始C#事件语法让它正常工作(因此订阅者可以执行 publisher.event += thedelegate 来弱连接事件)。尽管如此,我最终没有这样做,因为我认为如果将其弱绑定,则需要以不同的语法注册事件-不同的语法表示不同的语义。最后日志记录的事情很有趣,但我对于一个对象在终结器内操作其他托管对象感到非常谨慎。 - JMarsch

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