在Finalizer中处理MemoryCache会引发AccessViolationException

时间:2014-10-08 19:05:01

标签: c# asp.net idisposable finalizer memorycache

修改 有关其他详细信息,请参阅问题底部的编辑备注。

原始问题

我有一个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本身是Disposable遵循标准的Dispose(bool)模式)。

但是,当我的代码作为ASP.NET应用程序的一部分运行时,我发现这会导致问题。卸载应用程序域时,终结器将在我的CacheWrapper类上运行。这反过来试图处置MemoryCache实例。这是我遇到问题的地方。似乎Dispose试图从IIS加载一些配置信息失败(大概是因为我正在卸载app域,但我不确定。这是我的堆栈转储:

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

修改

This article验证了Dan Bryant的答案,并讨论了许多有趣的细节。特别是,他涵盖了StreamWriter的情况,该情况与我的情况类似,因为它希望在处置时冲洗它的缓冲区。这是文章所说的内容:

  

一般来说,终结器可能无法访问托管对象。   但是,必须支持关闭逻辑   合理复杂的软件。 Windows.Forms命名空间处理此问题   使用Application.Exit,启动有序关闭。什么时候   设计库组件,有一种方法是有帮助的   支持与现有集成的关闭逻辑   逻辑上相似的IDisposable(这避免了必须定义一个   IShutdownable接口,没有任何内置语言支持)。这个   通常是通过支持有序关机来完成的   调用IDisposable.Dispose,并在调用时中止关闭   不。如果终结器可以用来做更好的话会更好   尽可能有序地关闭。

     

微软也遇到了这个问题。 StreamWriter类   拥有一个Stream对象; StreamWriter.Close将刷新其缓冲区和   然后调用Stream.Close。但是,如果StreamWriter未关闭,则为其   终结器无法刷新其缓冲区。微软“解决了”这个问题   不给StreamWriter一个终结器,希望程序员能够   注意丢失的数据并推断出他们的错误。这是完美的   需要关闭逻辑的例子。

所有这一切,我认为应该可以使用WeakReference实现“托管终结”。基本上,让您的类在创建对象时向自己注册WeakReference,并在某个队列中执行finalize操作。然后,队列由后台线程或计时器监视,该计时器在配对WeakReference时调用适当的操作。当然,你必须要小心,你的最终动作不会无意中坚持课程本身,从而完全阻止了收集!

2 个答案:

答案 0 :(得分:7)

您无法在终结器中处理托管对象,因为它们可能已经完成(或者,正如您在此处看到的那样,部分环境可能不再处于您所处的状态。期待。)这意味着如果你包含一个必须显式处置的类,你的类也必须显式处置。没有办法欺骗&#39;并自动进行处置。不幸的是,在这种情况下,垃圾收集是一种漏洞抽象。

答案 1 :(得分:1)

我建议使用终结器的对象通常不应该暴露给外界,并且应该只对实际需要的事物进行强有力的引用,并且不会暴露于外界的任何不期望它们的东西。用于此目的。面向公众的类型本身不应该有终结器,而应该将清理逻辑封装在最终化类的私有实例中,其目的将封装这样的逻辑。

对于终结者来说,尝试清理另一个对象所拥有的资源时,唯一有意义的是当另一个对象设计与终结器接口时。我想不出任何框架类在适当的钩子中设计的地方,但会提供一个示例,Microsoft 可以如何设计它们。

File对象可以提供具有线程安全订阅和取消订阅方法的事件,当File对象收到Dispose调用或者finalize调用时,它会触发(先通知最后一个订阅者)一个Finalize请求。事件将在调用时间File和封装文件实际关闭的时间之间触发,并且可以被外部缓冲类用作它需要提供File任何信息的信号它收到但尚未传递。

请注意,为了使这样的事情正常且安全地工作,有必要将WeakReference对象的具有终结器的部分不暴露给公众,并且它使用长弱引用确保如果它在面向公众的对象仍然存在时运行,它将重新注册自己以进行最终确定。请注意,如果对Target对象的唯一引用存储在可终结对象中,则如果可终结对象符合最终确定条件,则其Dispose属性可能无效,即使引用的实际目标也是如此还活着。有缺陷的设计,恕我直言,以及必须仔细解决的设计。

可以设计具有可以合作的终结器的对象(最简单的方法,通常是,只有组中的一个对象运行终结器),但如果事情不是设计为与终结器配合,一个人可以做的最好的事情通常是有一个终结器声音警告,指示“这个对象不应该是{{1}}但是不是;因为它不是,资源会泄漏,并且没有什么可以除了修复代码以便将来正确处理对象“。