【问题标题】:Proper way of releasing COM objects?释放 COM 对象的正确方法?
【发布时间】:2013-03-21 15:44:15
【问题描述】:

有时当我结束应用程序并尝试释放一些 COM 对象时,我会在调试器中收到警告:

检测到RaceOnRCWCleanUp

如果我编写了一个使用 COM 对象的类,我是否需要在 IDisposable.Dispose 中实现 IDisposable 并在它们上调用 Marshal.FinalReleaseComObject 才能正确释放它们?

如果Dispose没有被手动调用,我还需要在终结器中释放它们还是GC会自动释放它们?现在我call Dispose(false) in the finalizer 但我想知道这是否正确。

我使用的 COM 对象也有一个事件处理程序,该类可以监听。显然该事件是在另一个线程上引发的,那么如果在处置类时触发它,我该如何正确处理它?

【问题讨论】:

  • 这提醒我们手动内存管理并不是一个好主意。 MDA 发出警告是因为 COM 服务器中潜在的硬崩溃。试图在应用程序关闭时处理任何东西是没有意义的,它将在一毫秒后完成。 GC 已经知道如何做到这一点,避免提供帮助。

标签: c# .net com


【解决方案1】:

根据我使用不同 COM 对象(进程内或进程外)的经验,我建议每个 COM/.NET 边界交叉使用一个 Marshal.ReleaseComObject(如果您引用 COM 对象以检索另一个 COM 引用)。

我遇到了很多问题,因为我决定将 COM 互操作清理推迟到 GC。 另请注意,我从不使用 Marshal.FinalReleaseComObject - 一些 COM 对象是单例的,它不适用于此类对象。

禁止在终结器内的托管对象中执行任何操作(或著名的 IDisposable 实现中的 Dispose(false))。您不能依赖终结器中的任何 .NET 对象引用。您可以释放IntPtr,但不能释放 COM 对象,因为它可能已经被清理了。

【讨论】:

  • 如果我使用 COM 对象的类在应用程序终止之前一直存在怎么办?如果 COM 对象的“文档”“需要”调用某种 Deactivate 方法? (我不确定它的作用,但我尝试不调用它,也没有发生任何不好的事情。)
【解决方案2】:

这里有一篇文章:http://www.codeproject.com/Tips/235230/Proper-Way-of-Releasing-COM-Objects-in-NET

简而言之:

1) Declare & instantiate COM objects at the last moment possible.
2) ReleaseComObject(obj) for ALL objects, at the soonest moment possible.
3) Always ReleaseComObject in the opposite order of creation.
4) NEVER call GC.Collect() except when required for debugging.

在GC自然发生之前,com引用不会被完全释放。这就是为什么这么多人需要使用 FinalReleaseComObject() 和 GC.Collect() 来强制对象销毁的原因。脏互操作代码都需要这两者。

GC 不会自动调用 Dispose。当一个对象被释放时,析构函数被调用(在不同的线程中)。这通常是您可以释放任何非托管内存或 com 引用的地方。

析构函数:http://msdn.microsoft.com/en-us/library/66x5fx1b.aspx

...当您的应用程序封装非托管资源(例如窗口、文件和网络连接)时,您应该使用析构函数来释放这些资源。当对象符合销毁条件时,垃圾收集器运行对象的 Finalize 方法。

【讨论】:

  • 为什么我们不应该在 GC.WaitForPendingFinalizers() 之后调用 GC.Collect()?
  • 我自己也不是 100% 确定,但是如果您阅读文章,它会说“GC.Collect 将停止操作系统中的所有 .NET 进程以收集垃圾。每次调用 GC.Collect 时,您可以将垃圾从第 0 代提升到第 1 代,然后再提升到第 2 代。一旦垃圾到达第 2 代,您必须终止进程以恢复内存。”所以,可能是因为 COM 对象可能会因为强制执行过多的 GC 调用而在 GC 中停留太久。
  • 更多的是频繁调用 GC.Collect 会阻止 GC 及时释放内存。这是因为 GC 对长寿命对象(即第 2 代)的检查频率较低;问题是每次 GC 跳过对象时都会提升对象的生成(即对该对象的引用仍然存在),因此通过调用 GC.Collect 对象会过早提升,从而欺骗 GC 认为它们甚至是长寿的如果不是。
  • 如果你说的和我说的完全一样,减去可能需要终止你的应用程序的部分(在很多情况下我认为这是真的)。我当然希望您没有因为不同意我的评论而对答案投反对票。如果您对答案本身有疑问,请解释。如果不是你,那么现在阅读这篇文章的人都应该这样做。
【解决方案3】:

首先 - 在进行 Excel 互操作时,您永远必须调用 Marshal.ReleaseComObject(...)Marshal.FinalReleaseComObject(...)。这是一个令人困惑的反模式,但任何有关此的信息(包括来自 Microsoft 的信息)表明您必须从 .NET 手动释放 COM 引用都是不正确的。事实上,.NET 运行时和垃圾收集器正确地跟踪和清理 COM 引用。

其次,如果要确保在进程结束时清除对进程外 COM 对象的 COM 引用(以便 Excel 进程将关闭),则需要确保垃圾收集器运行。您可以通过调用GC.Collect()GC.WaitForPendingFinalizers() 正确执行此操作。调用两次是安全的,end 确保循环也被清理干净。

第三,在调试器下运行时,局部引用会人为地保持活动状态,直到方法结束(这样局部变量检查才起作用)。因此,GC.Collect() 调用对于从同一方法清除像 rng.Cells 这样的对象无效。您应该将执行 COM 互操作的代码从 GC 清理中拆分为单独的方法。

一般模式是:

Sub WrapperThatCleansUp()

    ' NOTE: Don't call Excel objects in here... 
    '       Debugger would keep alive until end, preventing GC cleanup

    ' Call a separate function that talks to Excel
    DoTheWork()

    ' Now Let the GC clean up (twice, to clean up cycles too)
    GC.Collect()    
    GC.WaitForPendingFinalizers()
    GC.Collect()    
    GC.WaitForPendingFinalizers()

End Sub

Sub DoTheWork()
    Dim app As New Microsoft.Office.Interop.Excel.Application
    Dim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()
    Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")
    app.Visible = True
    For i As Integer = 1 To 10
        worksheet.Cells.Range("A" & i).Value = "Hello"
    Next
    book.Save()
    book.Close()
    app.Quit()

    ' NOTE: No calls the Marshal.ReleaseComObject() are ever needed
End Sub

关于这个问题有很多虚假信息和混淆,包括 MSDN 和 StackOverflow 上的许多帖子。

最终说服我仔细研究并找出正确建议的是这篇帖子https://blogs.msdn.microsoft.com/visualstudio/2010/03/01/marshal-releasecomobject-considered-dangerous/ 以及在某些 StackOverflow 答案的调试器下发现引用保持活跃的问题。

【讨论】:

  • 感谢您的评论,但我应该如何使用您的建议 - 从我所见,“DotheWork”是您使用 Excel 进行操作的 Sub,而“WrapperThatCleansUp”应该清理 COM 对象 - 但从你决定在哪一点调用这个 Sub ?如果你把它放在你用 Excel 做事情的地方(比如 2DoTheWork" Sub),那么你就会陷入循环。
  • 'WrapperThatCleansUp' 做了两件事:(1) 调用进行 COM 调用的 Sub,以及 (2) 调用垃圾收集器进行清理。更高级别的代码将调用“WrapperThatCleansUp”。像这个例子一样,你应该把 COM 工作放在一个单独的例程中,而不是你调用 GC 的那个例程,以确保局部变量的明确范围。
  • 这真是太棒了——有史以来最好的解决方案!实际上,我最近参加了 Microsoft 的一门课程,他们在这个问题上教授和推荐的内容完全不同,而且代码更长。他们的建议是: 1.) 创建一个实现 IDisposable 接口的类,并为里面的 COM 对象编写方法(用于创建文档、保存等)。 2.) 在任何时候使用 Using Statement 调用该类 - 这应该会处理对象被处置....但是,您的代码是我见过的最好的,也是最短的,非常感谢!
  • 只是提一下 - 你的代码有效,但只有当我在我的代码中包含 Marshal.ReleaseComObject() 时才对我来说 - 我做错了什么,你真的测试你的代码是 100% 确定吗?
  • 是的,除非您在调用 GC 之前将其明确设置为“无”,否则类级引用将使 Excel 保持活动状态。
猜你喜欢
  • 2023-03-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-03-04
  • 2015-09-25
  • 2012-09-17
  • 1970-01-01
相关资源
最近更新 更多