1

我正在寻找管理在 .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 对象的引用?

4

3 回答 3

1

但在实践中,我们的软件已经开始出现内存问题

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

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

于 2013-06-28T22:50:17.457 回答
1

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

  1. 将您的返回值定义为IntPtr. 然后,当您的托管方法准备好返回时,IntPtr通过Marshal.GetIUnknownForObject(). 此时,您可以安全地在您的 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;
        }
    }

于 2021-06-25T02:44:36.707 回答
0

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

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

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

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

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

于 2013-07-05T20:21:40.167 回答