4

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

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

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

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

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

编辑

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

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

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

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

4

4 回答 4

3

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

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


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

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

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

于 2010-07-26T17:38:30.343 回答
0

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

在 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;
            }
        }
    }
}
于 2010-07-26T18:58:11.390 回答
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;

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

于 2010-07-27T15:05:42.080 回答
0

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

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

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

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

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

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

编辑

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

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

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

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

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

于 2010-07-26T18:52:11.170 回答