垃圾回收器的隐藏力量:TTL缓存设计

在.NET环境中,垃圾回收器(GC)是一个强大的后台服务,它自动管理内存,回收不再使用的资源。然而,GC的这种能力往往被开发者忽视。本文将探讨如何利用GC的这种能力,设计一个高效的TTL(Time-to-Live)缓存系统。

设想一个系统,其中客户端与对等体(peer)进行通信。每个客户端到对等体的消息都需要一个连接。建立连接是一个重量级操作,希望缓存打开的连接。当特定连接的所有消息都发送完毕后,连接应该在一段时间后关闭。

典型的使用案例如下:

  1. 查找缓存中的对等体连接。
  2. 如果缓存中不存在连接,则打开连接并将其存储在缓存中。
  3. 使用连接向对等体发送消息。
然而,实现这些并不简单。为了缓存连接,需要将其存储在数据结构中,这会阻止它被垃圾回收。因此,需要为每个连接维护一个引用计数器。为此,需要公开Get/Release方法,这些方法将被一个使用IDisposable保护的using指令使用。如果消息跨多个线程传输,情况会更糟,因为using指令无法帮助处理线程问题。

实现上述所有内容感觉并不正确。感觉像是在重新发明垃圾回收器。不能直接使用.NET提供的垃圾回收器吗?

设计目标

目标是:一个GC感知的TTL缓存。

C# 接口定义如下: public interface ITtlCache where TFacet : class where TBody : class { void Store(TFacet facet, TBody body); TBody[] GetDeadbodies(TimeSpan ttl); }

TTL缓存的主要目的是将与垃圾回收器的交互和生存时间(TTL)问题封装起来,使其不会污染自己的缓存组件的设计和实现。

以下是这样一个缓存应该封装的易变性:

  • 非侵入式编程模型:不需要using和Get/Release方法。
  • 没有弱引用。
  • 没有基类。
  • 不需要订阅事件。
  • 垃圾回收器的交互:缓存与GC有紧密的交互,应该封装起来。
  • 垃圾回收器的异步性质:GC在其自己的线程中收集对象。任何GC通知将在其任意线程中传递。那个GC线程应该被封装。
  • 引用计数:显然缓存中有一些引用计数机制。应该封装起来。

还有一个易变性可能已经被封装了。这是将对象放入缓存和释放死亡对象的竞态条件。但经过思考,最终确信这种易变性属于TTL缓存的外部范围。

以下是TTL缓存的静态视图(参与者)。每个参与者附近的封装易变性已列出。

TTL缓存可以被认为是一种映射,将facet(轻量级对象)映射到body(重量级对象)。在系统中,消息是连接的facet。TTL缓存将对象包装在一个TtlItem中,并将其存储在私有存储库ItemsRepo中。然后TTL缓存为每个facet和body创建一个DeathNotifier。DeathNotifier将facet被垃圾回收的事件委托给TtlItem,其中维护了引用计数器。

TTL缓存的方法是线程安全的,可以在任意线程中调用。由于TTL缓存不公开任何事件,其客户端也不需要处理垃圾回收器线程。TTL缓存的状态存储在ItemsRepo类中,该类受到独占锁的保护。

以下是一个基本的单元测试示例: class Message {} class Connection {} [Test] public void Item_should_be_removed_after_its_death() { var cache = TtlCache.New(); var connection = new Connection(); var message = new Message(); cache.Store(message, connection); message = null; GC.Collect(); GC.WaitForPendingFinalizers(); Thread.Sleep(10.Milliseconds()); var bodies = cache.GetDeadBodies(1.Milliseconds()); Assert.That(bodies[0], Is.SameAs(connection)); }

工作原理

实现的核心部分是.NET核心类ConditionalWeakTable。它在.NET 4中添加,以帮助编译器为每个对象存储自定义属性。ConditionalWeakTable允许将任何属性(对象)与对等体(另一个对象)关联起来,以便当对等体被垃圾回收器收集时,关联的属性也被收集。实现的中心思想是将一个给定对象与一个带有析构函数的DeathNotifier类关联。因此,当给定对象被垃圾回收器收集时,关联的死亡通知器也被收集,其析构函数被调用。因此,析构函数有效地成为了一个给定对象被收集的事件。

DeathNotifier的本质如下: class DeathNotifier { private IReferenceCounter _reference; ~DeathNotifier() { var reference = _reference; if (reference != null) reference.ReleaseBody(); } }

任何给定对象都被内部的TtlItem包装,它实现了两个接口ITtlItem和IReferenceCounter。

TtlItem实现引用计数和死亡逻辑如下: class TtlItem : ITtlItem, IReferenceCounter where TBody : class { void IReferenceCounter.LinkBody() { int count = Interlocked.Increment(ref _referenceCounter); if (count == 1) _deadSince = DateTime.MaxValue; } void IReferenceCounter.ReleaseBody() { int count = Interlocked.Decrement(ref _referenceCounter); if (count == 0) _deadSince = DateTime.UtcNow; } }

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485