懒加载的替代方案 - LazyAndWeak 和 BackgroundLoader






4.96/5 (10投票s)
本文将介绍两种 Lazy 的替代方案。
引言
本文将介绍两种 Lazy
的替代方案。其中一种侧重于低内存使用,另一种侧重于响应速度。
背景
.NET 4 添加了 Lazy
类。它的目的是实现快速加载时间和减少内存消耗。
但是,我是否应该说我认为 Lazy
类... 太懒了?在使用 Lazy
类时,我看到了两个问题
- 应用程序加载速度很快,但之后您必须等待首次访问的每个项。
- 如果您加载一个项只使用一次,它将永远保留在内存中(好吧... 直到
lazy
对象本身被收集)。
因此,为了解决这些问题,我决定创建我自己的解决方案,我将在本文中介绍。
懒加载的替代方案
我真的很想创建一个既响应迅速又在不需要时避免使用内存的解决方案。但遗憾的是,这不可能... 至少对我来说,作为通用目的的解决方案。但我可以一次解决其中一个问题。
于是我做到了,作为两个不同的类
LazyAndWeak
- 更好的内存管理BackgroundLoader
- 更好的响应速度
LazyAndWeak
对于人类来说,懒惰通常是坏的。虚弱也是更糟糕的。但对于编程语言来说,懒惰和虚弱是好的,尤其是在我们想要节省内存时。
LazyAndWeak
只在第一次请求时创建其 Value
,就像 Lazy
类一样。但它也将值存储为弱引用,因此如果需要内存,它可以被回收。好吧,它比这稍微多一点。为了避免在每次垃圾回收时都回收值,我使用我的 GCUtils.KeepAlive
方法,这样最近使用的项就不会在下一次完整回收中被回收,因此持续使用的项将始终保留在内存中,即使在完整回收之后。
这里最大的缺点是,一旦项从内存中卸载,如果再次需要它,用户将需要再次等待,并且该项中存储的任何值都将丢失。因此,构造函数/创建者知道如何获取最新信息非常重要。
BackgroundLoader
BackgroundLoader
类与 LazyAndWeak
方向相反。它不是仅在请求时创建项然后允许它们消失,而是尝试在后台加载所有项,尽量不影响应用程序性能,然后将它们保留在那里。所以,您加载应用程序的速度非常快,类似于任何 Lazy
替代方案的情况,但在 CPU 空闲时,它会开始加载您尚未使用的项。
我说它方向相反是因为它不会节省内存,因为迟早所有项都会被加载并保留在内存中。但除了第一个项(如果用户在应用程序加载时就请求它)之外,用户无需等待项加载。用户打开应用程序(速度非常快),可能等待第一个操作加载其项,但之后,任何其他操作都将立即完成,因为项已经加载。
何时使用哪一个?
在我看来,永远不要使用 Lazy
。也许我太极端了,但我看不到 Lazy
单独会更好的实际情况。例如,如果您的应用程序有 1000 个菜单项,每个菜单项都有缓慢的加载时间,而您的用户可能只使用这 1000 个菜单项中的 10 个,那么这是可以接受的。
但是当用户加载这 1000 个项时会发生什么?它们将永远保留在内存中。所以,用户将需要等待 1000 次,然后它们将永远在那里(所以新的请求不会等待)。
但是,如果这 1000 个项消耗了过多的内存,我建议您使用 LazyAndWeak
,毕竟当计算机内存不足时,计算机运行速度会变慢。如果您认为将所有这些项保留在内存中是可以接受的,那么请使用 BackgroundLoader
,因为用户会等待程序加载,也许会等待第一个菜单项加载... 如果他们停留足够长的时间,当他们使用任何其他项时,它已经加载在内存中,无需再次等待。
事实上,对于非常大的项目,我通常会打开应用程序,出去一会儿,然后再回来,期望一切都已加载。我认为在 Visual Studio 中,当我第一次尝试加载 ASP.NET 或 WPF 项目时,仍然需要等待,这是令人沮丧的。
实现
我知道 CodeProject 是关于代码的... 但我真的应该在这里展示代码吗?我认为解释基本思想就足够了。所以,我只会解释这个想法。
LazyAndWeak
在创建值时使用锁定(因此两个线程不会创建重复值)。在访问 Value
时,会调用 GCUtils.KeepAlive
。我在我最早的文章之一 WeakReferences as a Good Caching Mechanism 中解释了那个类。仅使用 WeakReference
s 的主要问题是,即使是最近使用过的项,它们也会在每次回收时被丢弃。此类禁止它们在下一次回收时被回收,但如果发生第二次回收且它们仍未使用,则允许它们被回收。
BackgroundLoader
更难。它创建了一个“加载器”线程。每次创建 LazyLoader
时,它都会在该线程内注册自己并设置一个事件(ManagedAutoResetEvent
),以便加载器线程开始运行。如果有 1000 个项注册,它们将按顺序加载(不存在 1000 个线程尝试加载其项的风险)。此类在访问其 Value
时也使用锁定,因此如果尚未加载,它会立即加载并从 loader
类中删除此 BackgroundLoader
实例,因为它们已经被加载。
另外,还有一个问题是您可能会创建后台加载器然后将其丢弃(在示例项目中,当您从一个目录切换到另一个目录时)。这就是为什么我实现了 IDisposable
接口。如果项尚未加载,它就在“待加载”项的列表中。如果您丢弃它,它只会从“待加载”列表中删除该项。
如何使用这些类
使用这些类与使用 .NET Lazy
类非常相似。您只需在第一时间创建 LazyAndWeak<SomeType>
或 BackgroundLoader<SomeType>
,可能是这样的声明
private readonly LazyAndWeak<SomeType> field = new LazyAndWeak<SomeType>();
当您需要访问 SomeType 实例时,您必须访问已创建字段的 Value
属性。
重要的是 SomeType 具有默认构造函数,或者您将其提供了一个创建者委托,否则您将收到一个异常。另外,与 lazy
一样,这些辅助对象仅用于保存加载缓慢或内存占用大的对象。不要将它们用于快速小的对象,因为辅助对象本身会消耗内存。
示例
附带的示例是一个非常简单的图像查看器。它的目的只是展示各种技术的差异。
当您进入任何目录时,所有图像名称都会被读取,并执行某种类型的懒加载。当仅打开或滚动大型文件夹时,您将看到红线而不是图像。红线表示这些图像尚未加载到内存中。
如果您强制进行回收,已加载的图像将使用 LazyAndWeak
重新加载(这表明它有效),但不会对 BackgroundLoader
或自定义 Lazy
产生任何影响。
我故意强制图像加载时有 100 毫秒的延迟。这是为了让差异更明显,因为加载图像并不是那么慢。
如果您正在使用 BackgroundLoader
,当您更改目录时,它将停止加载旧文件。这就是为什么需要 Dispose
。如果我不这样做,如果我进入目录 A,然后是 B... B 将只会在加载完所有(无用的)A 文件后才加载。
历史
- 2011年12月1日:初始版本