【问题标题】:Why is this object being finalized while still being used?为什么这个对象在被使用的时候就被敲定了?
【发布时间】:2014-10-28 12:27:49
【问题描述】:

我正在尝试使用 DirectorySearcher 获取 AD 用户列表:

using (var entry = new DirectoryEntry("LDAP://mydomain.com"))
using (var search = new DirectorySearcher(entry))
{
    // Setup the query...
    search.PageSize = 1000;

    using (SearchResultCollection results = search.FindAll())
    {
        foreach (SearchResult result in results)
        {
            // Read the results and insert in a list
        }
    }
}

此查询最多可能需要一分钟,因此我正在线程池 (ThreadPool.QueueUserWorkItem) 中的一个线程中运行。查询工作正常,我得到了正确的结果。

但是,如果我在查询运行时关闭应用程序,我会系统地得到这个 MDA:RaceOnRCWCleanup was detected。确实,我的线程仍在运行,正在等待枚举继续:

[Managed to Native Transition]
System.DirectoryServices.dll!System.DirectoryServices.SearchResultCollection.ResultsEnumerator.MoveNext() + 0x4a bytes
MyApp.exe!MyApp.ActiveDirectory.FetchAllUsers() Line 125 + 0x4dd bytes

同时,终结器线程正在终结DirectoryEntry的一个实例:

mscorlib.dll!System.__ComObject.ReleaseSelf() + 0x5 bytes   
mscorlib.dll!System.Runtime.InteropServices.Marshal.ReleaseComObject(object o) + 0x84 bytes 
System.DirectoryServices.dll!System.DirectoryServices.DirectoryEntry.Unbind() + 0x27 bytes  
System.DirectoryServices.dll!System.DirectoryServices.DirectoryEntry.Dispose(bool disposing) + 0x29 bytes   
System.dll!System.ComponentModel.Component.Finalize() + 0x1b bytes

我可以验证(通过 Visual Studio 中的 Make Object ID)正在完成的 DirectoryEntrySearchResultCollection 使用的内部实例。

  • 为什么这个DirectoryEntry 在被使用的时候就已经定稿了?
  • 终结器是否知道这是一个将要退出的后台线程,并且是否将只能从该线程访问的对象视为可终结的?
  • 我能做点什么吗?在退出应用程序之前等待查询完成是不可接受的。

更新 1

尝试在foreach 之后添加GC.KeepAlive(results)。还尝试通过首先通过反射在results 中的内部DirectoryEntry 上执行GC.KeepAlive()。不走运:条目仍在最终确定中...

using (var entry = new DirectoryEntry("LDAP://mydomain.com"))
using (var search = new DirectorySearcher(entry))
{
    using (SearchResultCollection results = search.FindAll())
    {
        var rootEntryField = typeof(SearchResultCollection).GetField("rootEntry", BindingFlags.NonPublic | BindingFlags.Instance);
        var rootEntry = rootEntryField.GetValue(results);

        foreach (SearchResult result in results) // Callstack is here when entry is finalized
        {
            // Do something
        }

        GC.KeepAlive(results);
        GC.KeepAlive(rootEntry); // This is the entry being finalized
   }

   GC.KeepAlive(search);
   GC.KeepAlive(entry);
}

我注意到一个奇怪的细节是,有时,当我点击 MDA 时,SearchResultCollection 也已经被释放了。我认为它是通过最终确定处理的,因为我显然还没有调用Dispose()。在这种情况下,在我看来,GC 无法完成对象,因为我显然需要稍后处理它...

更新 2

我做了一个简单的测试,以确定当应用程序关闭而后台线程似乎仍在运行时,GC 是否可以从后台线程收集/完成对象。考虑这段代码在后台线程中运行:

using (new MyFinalizableType())
{
    System.Threading.Thread.Sleep(100000);
}

如果您在 using 块中关闭应用程序,您可以验证~MyFinalizableType() 在线程仍处于活动状态时被调用(您可以在“线程”窗口中看到它及其调用堆栈) .

因此,总而言之,似乎GC 确实认为这些对象是可收集的,在这种情况下,没有什么可以阻止RaceOnRCWCleanup MDA。

请注意,您可以GC.SuppressFinalize 有问题的对象,但是 COM 引用会泄漏,因为永远不会在对象上调用 Dispose() 方法。与内核对象/句柄不同,当进程退出时,无法回收 AFAIK COM 引用。

【问题讨论】:

  • 嗯,这是一个错误。由 DirectoryEntry.Unbind() 引起,它调用 Marshal.ReleaseComObject()。通常非常顽皮,当终结器调用 Unbind() 并且为已经终结的 COM 对象调用 ReleaseComObject 时,它会成为一个严重的错误。您无法修复错误,这是框架代码。 MDA 猜对并且您的程序将因 AccessViolationException 崩溃的可能性并不高,这发生在程序关闭时。因此,要么通过关闭 MDA 来忽略它,要么通过确保线程在退出之前结束来感觉更好。
  • 我想我坚持忽略 MDA.. 但这仍然不能解释为什么对象会首先完成。

标签: c# active-directory garbage-collection


【解决方案1】:

我不完全确定为什么DirectoryEntry 对象不再被认为是可访问的。我怀疑终结器对后台线程中的变量的处理方式不同。

我最好的猜测是,这是因为它通过互操作 COM 包装过程以某种方式断开连接,即它在技术上是可访问的,但只能通过对 GC 不透明的包装器。

您应该能够通过在foreach 循环之后添加GC.KeepAlive(entry); 来解决竞争条件。这将确保存储在entry 中的引用在该语句之前被认为是可访问的(否则,允许 JIT 编译器优化变量并导致对象更早变得不可访问)。

【讨论】:

  • foreach 之后添加GC.KeepAlive(entry) 不会改变任何东西。请注意,我必须通过反射使用私有字段SearchResultCollection.rootEntry 获取条目,因为它保留了自己的副本。
  • 您是否尝试过GC.KeepAlive(search)(或另外)?我知道你写了“不会改变任何东西”,但是entry 对象最终确定和search 对象最终确定之间的堆栈跟踪差异是微妙的,很容易被忽视。 (我有理由相信 GC 不会尝试最终确定可访问的对象,因此无论解决方案是什么,它都将归结为确保 GC 知道该对象是可访问的)。
  • 始终是最终确定的条目。更新了问题以显示 GC.KeepAlive 尝试。
【解决方案2】:

一个对象一旦不再被可执行代码访问,就可以被最终确定。在执行正在完成的对象的实例方法的过程中,可以满足该条件。如果从对该对象的唯一可访问引用调用该方法,则调用者在超出范围之前永远不会再次访问该变量,并且该方法的执行将不会在完成之前访问 this 变量(隐式或显式)方法然后该对象有资格完成。 (除其他原因外,这也是为什么处理最终确定很困难的原因。)

代码在线程池/后台线程中的事实与此处无关。

Here 是一篇关于该主题的博文,供进一步阅读。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-12-19
    • 2012-08-18
    • 2016-02-12
    • 2010-12-18
    相关资源
    最近更新 更多