何时在.Net中使用弱引用?


56

我个人没有遇到过需要在.Net中使用WeakReference类型的情况,但是普遍的看法似乎是应该在缓存中使用它。乔恩·哈罗普博士给了反对缓存使用在WeakReferences在他的一个很好的案例回答这个问题。

我也经常听到AS3开发人员谈论使用弱引用来节省内存占用,但是根据我的交谈,似乎增加了复杂性,而不一定实现预期的目标,并且运行时行为是不可预测的。如此之多,以至于许多人只是放弃了,而是更加谨慎地管理内存使用/优化其代码以减少内存占用(或权衡更多的CPU周期和较小的内存占用)。

乔恩·哈罗普(Jon Harrop)博士在回答中也指出.NET弱引用不是软的,并且在gen0上有激进的弱引用集合。根据MSDN,较长的弱引用使您有可能重新创建对象but the state of the object remains unpredictable.

考虑到这些特征,我想不出弱引用有用的情况,也许有人可以启发我?


3
您已经概述了它的潜在用途。当然,还有其他方法可以解决这些情况,但是有多种方法可以给猫做皮毛。如果您正在寻找一个防弹的“ X时应该始终使用WeakReference”,我怀疑您会找到一个。

2
@ itsme86-我不是在寻找一个防弹的用例,只是那些弱引用很适合并且很有意义的用例。例如,缓存用例,因为如此急切地收集了弱引用,所以即使您有足够的可用内存,也只会导致更多的缓存丢失

4
我有点失望,这要关闭很多票。我不介意看到答案或对此进行一些讨论(在b4“堆栈溢出不是论坛”中)。
ta.speot。是

@theburningmonk这是获取内存增益的偏移量。在当今的框架中,令人怀疑的是,即使在实现高速缓存时,任何人都可以直接使用WeakReference工具,因为现在可以使用全面的高速缓存系统。

是使用它们的一个令人尴尬的过于复杂的示例(针对ta.speot.is如下所述的弱事件模式)
Benjol 2013年

Answers:


39

我已经在以下三种实际情况中亲自发现了弱引用的合法实际应用:

应用程序1:事件处理程序

你是企业家 贵公司出售WPF 的火花线控制。销量不错,但支持费用使您丧命。太多的客户抱怨滚动满是火花线的屏幕时会出现CPU占用和内存泄漏的情况。问题在于他们的应用程序在出现时会创建新的火花线,但是数据绑定阻止了旧的火花线被垃圾收集。你是做什么?

在数据绑定和控件之间引入一个弱引用,以便仅数据绑定将不再阻止您的控件被垃圾回收。然后在您的控件中添加一个终结器,该终结器会在收集数据时拆解数据绑定。

应用2:可变图

您是下一位约翰·卡马克(John Carmack)。您已经发明了一个新颖的基于图形的新颖的分层细分曲面表示法,使Tim Sweeney的游戏看起来像任天堂Wii。显然,我不会确切告诉您它是如何工作的,但是所有这些都以该可变图为中心,在该图中可以在一个顶点上找到顶点的邻居Dictionary<Vertex, SortedSet<Vertex>>。当玩家四处奔跑时,图的拓扑结构不断变化。唯一的问题是:您的数据结构在运行时会丢失无法访问的子图,您需要删除它们,否则会泄漏内存。幸运的是您是一个天才,所以您知道有一类专门设计用来定位和收集无法访问的子图的算法:垃圾收集器!您读过理查德·琼斯(Richard Jones)关于该主题的出色专着但这会让您感到困惑,并担心您即将到来的截止日期。你是做什么?

只需Dictionary用一个弱的哈希表替换您,您就可以搭载现有的GC,并让它自动为您收集无法访问的子图!回到繁华的法拉利广告中。

应用3:装饰树木

您从键盘悬挂在圆形教室的天花板上。在有人找到您之前,您有60秒的时间浏览一些大数据。您已经准备好使用漂亮的基于流的解析器,该解析器依赖于GC在分析了AST的片段之后对其进行收集。但是您意识到每个AST上都需要额外的元数据,Node并且需要快速。你是做什么?

您可以使用a Dictionary<Node, Metadata>将元数据与每个节点相关联,但是,除非您将其清除,否则从字典到旧AST节点的强引用将使它们保持活动状态并泄漏内存。解决方案是使用弱哈希表,该仅保留对键的弱引用,并且当键变得不可访问时,垃圾会收集键-值绑定。然后,当AST节点变得不可访问时,将对其进行垃圾回收,并从字典中删除其键值绑定,从而使相应的元数据不可访问,因此也将其收集起来。然后,在主回路终止后,您所要做的就是从通风孔向后滑动,记得在安全警卫进来时将其更换。

请注意,在我实际遇到的所有这三个现实应用程序中,我都希望 GC尽可能积极地收集。这就是为什么这些是合法的应用程序。其他人都是错的。


2
如果无法访问的子图包含循环,则弱引用将不适用于应用程序2。这是因为弱哈希表通常具有对键的弱引用,但具有对值的强引用。您需要一个哈希表,该哈希表仅在键仍可访问时才维护对值的强引用->参见星历表(ConditionalWeakTable在.NET中)。
丹尼尔(Daniel)

@Daniel GC是否应该能够处理无法到达的循环?这将如何并不时强引用的可达周期收集被收集?
宾基

哦,我想我明白了。我只是假设这ConditionalWeakTable是应用程序2和3将使用的内容,而其他职位的某些人实际上会使用Dictionary<WeakReference, T>。不知道为什么-您总是会以大量的null结束,WeakReference而无论如何操作,任何键都无法访问它们。里迪克
宾基

@binki:“ GC是否应该能够处理无法到达的循环?当将收集到一个不完整的强引用循环时,怎么不收集呢?”。您有一个字典,其中包含无法重新创建的唯一对象。当您的一个关键对象变得不可访问时,可以将其垃圾回收,但是字典中的相应值在理论上甚至都不会被认为是不可访问的,因为普通的字典将对它有很强的引用,并使其保持生命。因此,您使用弱字典。
乔恩·哈罗普

@Daniel:“如果无法访问的子图包含循环,则弱引用将不适用于应用程序2。这是因为弱哈希表通常具有对键的弱引用,但具有对值的强引用。您需要一个散列表仅在密钥仍然可访问时才对这些值保持强引用”。是。您可能最好直接使用边缘作为指针对图形进行编码,以便GC自行收集。
乔恩·哈罗普

19

考虑到这些特征,我想不出弱引用有用的情况,也许有人可以启发我?

Microsoft文档弱事件模式

在应用程序中,附加到事件源的处理程序可能不会与将处理程序附加到源的侦听器对象协调地销毁。这种情况可能导致内存泄漏。Windows Presentation Foundation(WPF)通过为特定事件提供专用的管理器类并在该事件的侦听器上实现接口,引入了可用于解决此问题的设计模式。这种设计模式称为弱事件模式。

...

弱事件模式旨在解决此内存泄漏问题。每当侦听器需要注册事件时都可以使用弱事件模式,但是该侦听器不明确知道何时注销。每当源的对象生存期超过侦听器的有用对象生存期时,也可以使用弱事件模式。(在这种情况下,有用由您决定。)弱事件模式允许侦听器注册和接收事件,而不会以任何方式影响侦听器的对象生存期特征。实际上,来自源的隐式引用无法确定侦听器是否符合垃圾收集的条件。该引用是一个弱引用,因此是弱事件模式和相关API的命名。侦听器可以被垃圾回收或以其他方式销毁,并且源可以继续而不会保留对现已销毁的对象的不可收集的处理程序引用。


该URL自动选择其中的“该主题不再可用”的最新.NET版本(当前为4.5)。选择.NET 4.0,而不是作品(msdn.microsoft.com/en-us/library/aa970850(v=vs.100).aspx
MAXP

13

让我先把它放回去:

当您要在对象上保留选项卡,但又不想让观察结果阻止该对象被收集时,WeakReference很有用。

因此,让我们从头开始:

-对任何无意的冒犯表示歉意,但是我暂时要回到“迪克和简”的级别,因为永远无法告诉观众。

因此,当您有一个对象时X-让我们将其指定为-的实例class Foo-它就不能靠它自己生存(大多数情况下是这样);就像“没有人是一个岛”一样,只有几种方法可以将对象提升为Islandhood -尽管在CLR语言中它被称为GC根。成为GC根,或已建立到GC根的连接/引用链,基本上决定了是否Foo x = new Foo()收集垃圾。

如果您无法通过堆或堆栈遍历返回某些GC根目录,那么您实际上是孤立的,可能会在下一个周期被标记/收集。

在这一点上,让我们看一些可怕的例子:

首先,我们的Foo

public class Foo 
{
    private static volatile int _ref = 0;
    public event EventHandler FooEvent;
    public Foo()
    {
        _ref++;
        Console.WriteLine("I am #{0}", _ref);
    }
    ~Foo()
    {
        Console.WriteLine("#{0} dying!", _ref--);
    }
}

相当简单-它不是线程安全的,所以不要尝试这样做,而是在活动实例和减量完成时保留它们的“引用计数”。

现在让我们来看一个FooConsumer

public class NastySingleton
{
    // Static member status is one way to "get promoted" to a GC root...
    private static NastySingleton _instance = new NastySingleton();
    public static NastySingleton Instance { get { return _instance;} }

    // testing out "Hard references"
    private Dictionary<Foo, int> _counter = new Dictionary<Foo,int>();
    // testing out "Weak references"
    private Dictionary<WeakReference, int> _weakCounter = new Dictionary<WeakReference,int>();

    // Creates a strong link to Foo instance
    public void ListenToThisFoo(Foo foo)
    {
        _counter[foo] = 0;
        foo.FooEvent += (o, e) => _counter[foo]++;
    }

    // Creates a weak link to Foo instance
    public void ListenToThisFooWeakly(Foo foo)
    {
        WeakReference fooRef = new WeakReference(foo);
        _weakCounter[fooRef] = 0;
        foo.FooEvent += (o, e) => _weakCounter[fooRef]++;
    }

    private void HandleEvent(object sender, EventArgs args, Foo originalfoo)
    {
        Console.WriteLine("Derp");
    }
}

因此,我们有一个对象已经是它自己的GC根了(具体来说,它将通过一条直接链接到运行此应用程序的应用程序域的根,但这是另一个主题),它具有两种方法锁住Foo实例的方法-让我们对其进行测试:

// Our foo
var f = new Foo();

// Create a "hard reference"
NastySingleton.Instance.ListenToThisFoo(f);

// Ok, we're done with this foo
f = null;

// Force collection of all orphaned objects
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

现在,从上面开始,您是否希望曾经被引用的对象f是“可收集的”?

不,因为现在有另一个对象拥有对该对象的引用- Dictionary在该Singleton静态实例中。

好吧,让我们尝试一下弱方法:

f = new Foo();
NastySingleton.Instance.ListenToThisFooWeakly(f);

// Ok, we're done with this foo
f = null;

// Force collection of all orphaned objects
// This should collect # 2 - you'll see a "#2 dying"
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

现在,当我们重用Foo-the-was-once-的f引用时,不再有对该对象的“硬”引用,因此它是可收集的- WeakReference弱侦听器创建的对象不会阻止这种情况。

良好的用例:

  • 事件处理程序(尽管请先阅读以下内容:C#中的弱事件

  • 您可能会导致“递归引用”(即,对象A引用对象B,后者引用对象A,也称为“内存泄漏”),这种情况(编辑:derp,当然这不是不是真的)

  • 您想将某事物“广播”到一组对象中,但是您不想成为使它们存活的事物。一个List<WeakReference>能够容易地保持,甚至修剪通过去除其中ref.Target == null


1
关于第二个用例,垃圾收集器可以很好地处理循环引用。“对象A引用对象B,而对象B引用对象A”绝对不是内存泄漏。
Joe Daley

@JoeDaley我同意。.NET GC使用标记和清除算法(我相信我正确地记得了这一点)将所有对象标记为要收集,然后遵循“根”的引用(堆栈中对象的引用,静态对象),未标记要收集的对象。如果存在循环引用,但从根目录无法访问任何对象,则这些对象并非未标记为要收集,因此符合收集条件。
ta.speot。是

1
@JoeDaley-你们俩都是正确的-一直逼到最后...我将其编辑掉。
JerKimball

4

在此处输入图片说明

就像逻辑泄漏真的很难追踪,而用户只是注意到长时间运行软件往往会占用越来越多的内存,并且越来越慢,直到重新启动?我不。

考虑以下情况,如果当用户请求删除上述应用程序资源时,Thing2在以下情况下未能正确处理此类事件:

  1. 指针
  2. 强大的参考
  3. 参考文献薄弱

...在这种情况下,可能会在测试中发现其中一个这样的错误,而不会并且像隐身战斗机的虫子一样在雷达下飞行。所有权比大多数情况下更荒谬。


1

使用弱引用来取得良好效果的一个非常说明性的示例是ConditionalWeakTable,它由DLR(以及其他地方)用来将其他“成员”附加到对象上。

您不希望表格使对象保持活动状态。没有弱引用,这个概念根本行不通。

但是在我看来,弱引用的所有使用都是在将弱引用添加到语言中之后才出现的,因为弱引用自1.1版以来就是.NET的一部分。似乎您想要添加一些东西,因此就语言功能而言,缺乏确定性破坏不会使您陷入困境。


我实际上发现,尽管表使用了弱引用的概念,但是实际的实现并不涉及WeakReference类型,因为情况要复杂得多。它使用CLR公开的不同功能。
GregRos

-2

如果您使用C#实现了缓存层,则最好将数据作为弱引用放入缓存中,这可能有助于提高缓存层性能。

认为该方法也可以应用于会话实施。由于会话在大多数情况下都是长期存在的对象,因此在某些情况下您可能没有足够的存储空间供新用户使用。在这种情况下,最好删除其他用户会话对象,然后抛出OutOfMemoryException。

另外,如果您的应用程序中有一个大对象(一些大的查找表等),则应很少使用该对象,并且重新创建这样的对象并不是一个非常昂贵的过程。然后最好像一周的参考资料一样,在真正需要时释放内存。


5
但是弱引用(请参阅我所引用的答案)的问题在于,它们非常热切地被收集,并且该收集与内存空间的可用性无关。因此,当没有内存压力时,最终会导致更多的缓存丢失。

1
但是,关于大型对象的第二点,MSDN文档指出,虽然长时间的弱引用使您可以重新创建对象,但状态仍然不可预测。如果您打算每次从头开始重新创建它,为什么当您只需要调用一个函数/方法按需创建它并返回一个瞬态实例时,为什么还要使用一个弱引用呢?

在一种情况下,缓存是有用的:如果经常创建不可变的对象,而其中的许多对象恰好是相同的(例如,从文件中读取许多行,而这些文件可能会有很多重复),则每个字符串都将被创建为一个新对象。 ,但是如果某行与引用已经存在的另一行匹配,则在放弃新实例并替换对先前实例的引用的情况下,可以提高内存效率。请注意,此替换很有用,因为无论如何仍保留另一个引用。如果不是,则应保留新的代码。
2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.