在终结器中释放MemoryCache会抛出AccessViolationException异常。

8

编辑 请参见问题底部的编辑说明以获取其他细节。

原始问题

我有一个CacheWrapper类,它在内部创建并持有.NET MemoryCache类的实例。

MemoryCache将自己挂钩到AppDomain事件中,因此除非显式处理,否则永远不会被垃圾回收。您可以使用以下代码进行验证:

Func<bool, WeakReference> create = disposed => {
    var cache = new MemoryCache("my cache");
    if (disposed) { cache.Dispose(); }
    return new WeakReference(cache);
};

// with false, we loop forever. With true, we exit
var weakCache = create(false);
while (weakCache.IsAlive)
{
    "Still waiting...".Dump();
    Thread.Sleep(1000);
    GC.Collect();
    GC.WaitForPendingFinalizers();
}
"Cleaned up!".Dump();

因为这种行为,我认为我的MemoryCache实例应该被视为一个非托管资源。换句话说,我应该确保在CacheWrapper的终结器中对其进行处理(CacheWrapper本身是可处理的,并遵循标准的Dispose(bool)模式)。
然而,我发现当我的代码作为ASP.NET应用程序的一部分运行时,会出现问题。当应用程序域被卸载时,终结器会在我的CacheWrapper类上运行。这将尝试处理MemoryCache实例。这就是我遇到问题的地方。看起来Dispose尝试从IIS加载一些配置信息,但失败了(可能是因为我正在卸载应用程序域,但我不确定)。下面是我拥有的堆栈转储:
MANAGED_STACK: 
    SP               IP               Function
    000000298835E6D0 0000000000000001 System_Web!System.Web.Hosting.UnsafeIISMethods.MgdGetSiteNameFromId(IntPtr, UInt32, IntPtr ByRef, Int32 ByRef)+0x2
    000000298835E7B0 000007F7C56C7F2F System_Web!System.Web.Configuration.ProcessHostConfigUtils.GetSiteNameFromId(UInt32)+0x7f
    000000298835E810 000007F7C56DCB68 System_Web!System.Web.Configuration.ProcessHostMapPath.MapPathCaching(System.String, System.Web.VirtualPath)+0x2a8
    000000298835E8C0 000007F7C5B9FD52 System_Web!System.Web.Hosting.HostingEnvironment.MapPathActual(System.Web.VirtualPath, Boolean)+0x142
    000000298835E940 000007F7C5B9FABB System_Web!System.Web.CachedPathData.GetPhysicalPath(System.Web.VirtualPath)+0x2b
    000000298835E9A0 000007F7C5B99E9E System_Web!System.Web.CachedPathData.GetConfigPathData(System.String)+0x2ce
    000000298835EB00 000007F7C5B99E19 System_Web!System.Web.CachedPathData.GetConfigPathData(System.String)+0x249
    000000298835EC60 000007F7C5BB008D System_Web!System.Web.Configuration.HttpConfigurationSystem.GetApplicationSection(System.String)+0x1d
    000000298835EC90 000007F7C5BAFDD6 System_Configuration!System.Configuration.ConfigurationManager.GetSection(System.String)+0x56
    000000298835ECC0 000007F7C63A11AE System_Runtime_Caching!Unknown+0x3e
    000000298835ED20 000007F7C63A1115 System_Runtime_Caching!Unknown+0x75
    000000298835ED60 000007F7C639C3C5 System_Runtime_Caching!Unknown+0xe5
    000000298835EDD0 000007F7C7628D86 System_Runtime_Caching!Unknown+0x86
    // my code here

这个问题有已知的解决方案吗?我是否正确地认为我需要在终结器中处理MemoryCache

编辑

这篇文章验证了Dan Bryant的答案,并讨论了许多有趣的细节。特别是,它涵盖了StreamWriter的情况,因为它想要在处理时刷新缓冲区,与我的情况类似。以下是文章的内容:

一般来说,终结器不能访问托管对象。然而,对于相对复杂的软件,支持关闭逻辑是必要的。Windows.Forms命名空间使用Application.Exit处理这个问题,它启动一个有序的关闭过程。当设计库组件时,最好能够集成与现有的逻辑上类似的IDisposable的关闭逻辑方式(这样可以避免定义IShutdownable接口而没有任何内置语言支持)。通常情况下,当调用IDisposable.Dispose时支持有序关闭,否则支持中止关闭。如果可能的话,使用终结器进行有序关闭会更好。Microsoft也遇到了这个问题。StreamWriter类拥有Stream对象;StreamWriter.Close将刷新其缓冲区,然后调用Stream.Close。但是,如果未关闭StreamWriter,则其终结器无法刷新其缓冲区。Microsoft通过不为StreamWriter提供终结器来“解决”此问题,希望程序员注意到缺失的数据并推断出他们的错误。这是需要关闭逻辑的完美例子。

所有这些说法,我认为应该可以使用WeakReference实现“托管终结”。基本上,当对象创建时,让您的类向自己注册一个WeakReference和一个finalize操作,并将其与某个队列一起使用。然后,由后台线程或计时器监视队列,当它配对的WeakReference被收集时,调用相应的操作。当然,您必须小心,确保您的finalize操作不会意外地保留类本身,从而完全防止收集!


2
我非常确定在终结器中不应该处理托管对象,而是应该在正常的 IDisposable.Dispose 中处理。终结器只应该清理非托管对象。 - juharr
2
@juharr:在这种情况下,对象是“未托管”的,这意味着除非显式处理,否则它永远不会被垃圾回收。 - ChaseMedallion
@juharr:一个被管理的资源是一个持有外部资源的对象,但如果它被废弃,将会释放这些资源。那些持有外部资源但如果被废弃却不会释放资源的东西需要被视为非托管资源,尽管在许多情况下它们不能通过终结器进行清理,所以唯一的解决办法就是确保它们从一开始就不会被废弃。 - supercat
2个回答

7

在终结器中无法处理托管对象,因为它们可能已经被终结(或者,正如您在这里看到的那样,环境的部分状态可能不再是您期望的状态)。这意味着,如果您包含一个必须显式释放的类,则您的类也必须显式释放。没有办法“欺骗”并使释放自动化。不幸的是,在这种情况下,垃圾回收是一个有漏洞的抽象。


1
有趣。这个规则有没有明确的文档?我读过的东西总是使用术语“非托管资源”,我理解为任何不能被GC清理的东西。这是否意味着你基本上只能在终结器中处理操作系统句柄?例如,如果我有一个字符串路径表示要删除的文件,那么我的终结器引用该字符串以便删除该文件是否有效? - ChaseMedallion
你真的不应该在终结器中运行任何没有明确使用句柄(如IntPtr)关闭非托管资源的代码。在终结器线程上执行任何类型的I/O都是一个非常糟糕的想法,可能会导致你正在看到的问题。当终结器运行时,你无法知道安全上下文将是什么样子。 - Mike Strobel
@MikeStrobel 感谢您提供的额外细节。您能指出任何官方文档吗? - ChaseMedallion
@ChaseMedallion 在MSDN文档中关于Object.Finalize有很多有用的信息。一些建议,比如不执行I/O操作,可能需要读者自行揣摩:你无法确定最终化会在何时、何线程上发生,这对于任何阻塞和/或具有安全要求的操作都会产生影响。 - Mike Strobel
据我所知,微软从未真正定义“非托管资源”。它提供了一些示例,但没有定义。我会像你一样定义它:一个外部资源,如果对象被放弃,它不会自动清理。 - supercat

1
我建议具有finalizers的对象通常不应该向外界公开,并且只应该对实际需要进行终结的内容保持强引用,而这些内容不会暴露给外界任何未预期使用它们进行终结的内容。公共类型本身不应该有finalizers,而应该封装清理逻辑在私有的finalizable类实例中,其目的是封装此类逻辑。
唯一真正有意义的情况是finalizer尝试清理由另一个对象拥有的资源时,当另一个对象被设计为与finalizer接口时。我想不出任何Framework类已经设计好钩子的地方,但会提供一个示例,说明Microsoft如何可以设计它们来实现这一点。
一个File对象可以提供一个带有线程安全的订阅和取消订阅方法的事件,当File对象收到Dispose调用或finalize请求时触发(通知最后一个订阅者)。该事件将在调用Finalize和封装文件实际关闭之间触发,并可被外部缓冲类用作信号,表示它需要向File传递其已接收但尚未传递的任何信息。
请注意,为了使这样的事情正常安全地工作,必须确保File对象的具有终结器的部分不对外公开,并使用长弱引用来确保如果在公共面向对象仍然存活时运行,则会重新注册自身以进行终止处理。请注意,如果对WeakReference对象的唯一引用存储在可终止对象中,则即使引用的实际目标仍然存活,如果可终止对象变为可终止状态,则可能会使其Target属性无效。在我看来,这是有缺陷的设计,必须小心地解决。

可以设计具有终结器的对象来进行协作(通常最简单的方法是只让组中的一个对象拥有终结器),但如果事物没有被设计为与终结器协作,则通常最好的方法是让终结器发出警报,指示“这个对象不应该被Dispose,但实际上没有被处理;因为它没有被处理,资源将会泄漏,并且除了修复代码以正确处理对象之外,无法做任何事情”。


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