【问题标题】:Marshal.ReleaseComObject and return valuesMarshal.ReleaseComObject 和返回值
【发布时间】:2013-06-28 20:53:25
【问题描述】:

我正在寻找管理在 .Net 程序集中创建的 COM 对象的生命周期的想法,然后将其传递回非托管代码。

背景: 我们的软件引擎是用非托管 c++ 编码的。我们软件的功能可以使用引擎的 COM 对象组件进行扩展。在大多数情况下,引擎本身多年来不需要改变。但是,我们将继续构建更多组件以向我们的软件添加新功能。虽然这些组件过去也是用非托管 c++ 构建的,但在过去的几年里,我们一直在用 C# 编写这些插件。

直到最近,我们才赞同 Marshal.ReleaseComObject 不应该在 C# 组件中使用的非托管 COM 对象上调用的想法,而是允许 RCW 和垃圾回收管理它们的生命周期。

这个理论听起来不错,但实际上我们的软件已经开始出现内存问题,这似乎是由于 GC 懒于清理这些 COM 对象,以至于在某些情况下我们的软件会耗尽所有可用内存失败了。

我们是否可能犯了一些错误,导致 GC 和 RCW 无法对这些对象进行操作?垃圾收集的想法不是会做必要的事情来确保内存在需要时是空闲的吗?

由于没有更好的想法,我们不情愿地开始使用 Marshal.ReleaseComObject(在某些情况下是 FinalReleaseComObject)。一般来说,这似乎可以解决问题。

但是,在尝试开发一致的模式来使用 Marshal.ReleaseComObject 时,我们发现了一种情况,我们不确定是否可以使用它。某些组件需要将 COM 对象返回值传递回非托管引擎。该组件将永远不会再次看到返回值,并且(当前)在不再使用它时无法收到通知。例如:

// Unmanaged C++ code
Engine::Engine() : m_ipDoodadCreator(CLSID_FancyComponent)
{
}
void Engine::ProcessDoodad()
{
    try
    {
        // Smart pointer
        IDoodadPtr ipDoodad = m_ipDoodadCreator->CreateDoodad();
        ...
        ipDoodad = NULL; // Calls Release...
    }
    catch (...) { ... }
    // At this point the doodad lives on because the RCW is holding a
    // reference to it.
}

// Managed C# Component
public class FancyComponent : IDoodadCreator
{
    public IDoodad IDoodadCreator.CreateDoodad()
    {
        // IDoodad is implmented in unmanaged c++
        IDoodad doodad = new IDoodad();

        try
        {
            ...

            return doodad;
        }
        finally
        {
            // Can't do this here because engine doesn't yet have
            // a reference to doodad.
            // Marshal.ReleaseComObject(doodad);
        }
    }
}

到目前为止,为了在这种情况下确定性地让 RCW 释放其引用,我们能想出的唯一想法是重新设计引擎,以便所有接口都有一个 Cleanup() 调用,该调用被调用有时,但这会很耗时,在某些情况下,实施起来很麻烦。

tl;dr 有没有办法强制 RCW 释放其对返回到非托管环境中的 COM 对象的引用?

【问题讨论】:

  • IDoodad 应该从IUnknown 继承,所以你应该能够正常调用Release 我上次检查
  • Release 减少引用计数。如果其他东西(RCW)仍然有参考,doodad 继续存在。在上面的代码中,我暗示 IDoodadPtr 是一个智能指针,当它超出范围时调用 Release。
  • 你的意思是COM Callable Wrapper?根据MSDN,如果正常调用release,它将释放对象进行垃圾回收。
  • 是的,我不否认它有资格进行垃圾收集。但垃圾收集似乎并不总是及时发生。这就是我们试图以调用 ReleaseComObject 的形式进行干预的原因。但是当对象是返回值时怎么办呢?编辑:如果我有我的 RCW 和 CCW 的直通,我认为这是我们在这里谈论的运行时可调用包装器,而不是 CCW,因为在我的示例中,Doodad 是非托管的 c++ COM 对象。
  • 尝试手动管理调试会话的生命周期,看看Release 在您希望释放所有其他引用时返回的内容。 AddRef 很可能被调用了太多次。

标签: .net com-interop


【解决方案1】:

但实际上我们的软件已经开始出现内存问题

这是一个典型的成长痛问题。软件永远不会变小,团队中没有人因编写负代码而获奖。添加越来越多的功能,您的程序永远不会使用 less 内存。一个标准的诊断是消耗第一个千兆字节的虚拟内存空间需要很长时间。第二个千兆字节很快就消失了。

您现在的位置非常没有生产力,您正在为几兆字节而烦恼。只需将开关拨到没有问题的位置即可。将本机代码编译为 64 位。您不会从托管代码中得到任何反对,x64 抖动已经知道如何在没有任何更改的情况下做到这一点。

【讨论】:

  • 我认为您是对的,我们需要寻找其他策略。我们不久前使用的一种方法是打开大地址感知标志。但是,虽然采用 x64 可能不会“无效率”,因为我们可以获得长期利益,但它也绝非易事,而且我们也无法抽出时间来解决手头的紧迫问题;我们有很多遗留的 c++ 代码,其中类型的选择并没有考虑到最终迁移到 x64...
【解决方案2】:

我已经确定了两种可能的解决方案。两者都涉及更改您的 COM 接口的 托管 定义,尽管您根本不需要更改您的 非托管 代码。

  1. 将您的返回值定义为IntPtr。然后,当您的托管方法准备好返回时,通过Marshal.GetIUnknownForObject() 从您的COM 对象获取IntPtr。此时,您可以安全地在您的 COM 对象上调用“Marshal.ReleaseComObject()”。
    public interface IDoodadCreator
    {
        IntPtr IDoodadCreator.CreateDoodad();
    }

    public class FancyComponent : IDoodadCreator
    {
        public IntPtr IDoodadCreator.CreateDoodad()
        {
            // IDoodad is implemented in unmanaged c++
            IDoodad doodad = new IDoodad();
            ...
    
            IntPtr doodadPtr = Marshal.GetIUnknownForObject(doodad);
            Marshal.ReleaseComObject(doodad);
            return doodadPtr;
        }
    }

  1. 将您的返回值定义为object,并定义接口以使用自定义封送器,该封送器在将对象封送至 IntPtr 后释放该对象。
    public interface IDoodadCreator
    {
        [return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(ReleasingMarshaler))]
        object IDoodadCreator.CreateDoodad();
    }

    public class FancyComponent : IDoodadCreator
    {
        public object IDoodadCreator.CreateDoodad()
        {
            // IDoodad is implemented in unmanaged c++
            IDoodad doodad = new IDoodad();
            ...
    
            return doodad;
        }
    }

    public class ReleasingMarshaler : ICustomMarshaler
    {
        ...

        public IntPtr MarshalManagedToNative(object managedObj)
        {
            IntPtr managedObjPtr = Marshal.GetIUnknownForObject(managedObj);
            Marshal.ReleaseComObject(managedObj);
            return managedObjPtr;
        }
    }

【讨论】:

    【解决方案3】:

    我们当前问题的答案是 GC.AddMemoryPressure。

    因为我们通过非托管类的 RCW 实例创建和引用,所以垃圾收集器不知道为这些对象分配的内存。 根据有关此方法的 MSDN 帮助:

    在确定何时安排垃圾回收时,运行时会考虑分配了多少托管内存。如果一个小的托管对象分配了大量的非托管内存,运行时只考虑托管内存,从而低估了调度垃圾回收的紧迫性。

    http://msdn.microsoft.com/en-us/library/system.gc.addmemorypressure.aspx

    换句话说,我们不需要显式释放 COM 对象……我们只需要告诉 GC 有多少内存与它们相关联。事实上,在一些关键的地方使用这种方法后,垃圾收集器的表现会好很多。

    【讨论】:

    • 我不经常发帖,但添加评论来解释为什么你对答案投了反对票似乎是一种很好的礼仪,这样作者就可以理解什么是不正确/无用的......
    猜你喜欢
    • 1970-01-01
    • 2012-09-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-24
    • 2014-12-13
    • 2012-04-02
    相关资源
    最近更新 更多