在非托管资源上执行P / Invoke时需要GC.KeepAlive(this)吗?

时间:2019-06-01 09:52:21

标签: c# callback garbage-collection pinvoke finalizer

我有一个TestNet包装器用于本机组件。本机组件公开了一个 blocking TestNative::Foo()(可通过调用托管回调与托管部分进行通信)和一个弱GCHandle(用于检索对.NET包装器的引用)并提供一个上下文。 {。{1}}是弱的,因为.NET包装器旨在向用户隐藏正在处理非托管资源并且故意不实现GCHandle接口的事实:如果是非弱的,它将防止{{1} }实例被完全收集,从而造成内存泄漏。发生的事情是,在 Release 版本中,甚至在IDisposable和令人惊讶的TestNet解除阻塞之前,只有垃圾收集器会在执行托管回调时收集对.NET包装的引用。我了解了自己的问题,可以通过在P / Invoke调用后发出TestNative::Foo()来解决此问题,但是由于对此问题的了解还不是很广泛,所以似乎很多人都在做错了。我有几个问题:

  1. 如果最后一条指令是对非托管资源的P / Invoke调用,则在托管方法中始终需要TestNet::Foo(),或者在这种特殊情况下才需要GC.KeepAlive(this),即在从本机封送托管回调时切换到托管执行上下文码?问题可能是:我应该到处放置GC.KeepAlive(this)吗?这个旧的Microsoft blog(原始链接是404,这里是cached)似乎暗示了!但这将改变游戏规则,并且从根本上讲,这将意味着大多数人从未正确执行过P / Invoke,因为这将需要检查包装中的大多数P / Invoke调用。例如,是否有一条规则说垃圾收集器( EDIT :或更佳的终结器)不能在执行上下文不受管理(本机)的情况下为属于当前线程的对象运行?
  2. 在哪里可以找到适当的文档?我可以找到CodeAnalysis策略CA2115,它在通过P / Invoke访问非托管资源时任何都可以使用GC.KeepAlive(this)。通常,在处理finalizers时几乎不需要GC.KeepAlive(this)
  3. 为什么仅在Release版本中会发生这种情况?看起来像是一种优化,但在Debug构建中根本不需要,这隐藏了垃圾收集器的重要行为。

注意:我对收集代表没有任何问题,这是另一个问题,我知道如何正确处理。这里的问题是当P / Invoke调用尚未完成时,将收集保存非托管资源的对象。

下面的代码清楚地表明了问题所在。创建一个C#控制台应用程序和一个C ++ Dll1 项目,并以发布模式构建它们:

Program.cs

GC.KeepAlive(this)

Dll1.cpp

using System;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var test = new TestNet();
            try
            {
                test.Foo();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }
        }
    }

    class TestNet
    {
        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
        delegate void Callback(IntPtr data);

        static Callback _callback;

        IntPtr _nativeHandle;
        GCHandle _thisHandle;

        static TestNet()
        {
            // NOTE: Keep delegates references so they can be
            // stored persistently in unmanaged resources
            _callback = callback;
        }

        public TestNet()
        {
            _nativeHandle = CreateTestNative();

            // Keep a weak handle to self. Weak is necessary
            // to not prevent garbage collection of TestNet instances
            _thisHandle = GCHandle.Alloc(this, GCHandleType.Weak);

            TestNativeSetCallback(_nativeHandle, _callback, GCHandle.ToIntPtr(_thisHandle));
        }

        ~TestNet()
        {
            Console.WriteLine("this.~TestNet()");
            FreeTestNative(_nativeHandle);
            _thisHandle.Free();
        }

        public void Foo()
        {
            Console.WriteLine("this.Foo() begins");
            TestNativeFoo(_nativeHandle);

            // This is never printed when the object is collected!
            Console.WriteLine("this.Foo() ends");

            // Without the following GC.KeepAlive(this) call
            // in Release build the program will consistently collect
            // the object in callback() and crash on next iteration 
            //GC.KeepAlive(this);
        }

        static void callback(IntPtr data)
        {
            Console.WriteLine("TestNet.callback() begins");
            // Retrieve the weak reference to self. As soon as the istance
            // of TestNet exists. 
            var self = (TestNet)GCHandle.FromIntPtr(data).Target;
            self.callback();

            // Enforce garbage collection. On release build
            self = null;
            GC.Collect();
            GC.WaitForPendingFinalizers();
            Console.WriteLine("TestNet.callback() ends");
        }

        void callback()
        {
            Console.WriteLine("this.callback()");
        }

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern IntPtr CreateTestNative();

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern void FreeTestNative(IntPtr obj);

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern void TestNativeSetCallback(IntPtr obj, Callback callback, IntPtr data);

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern void TestNativeFoo(IntPtr obj);
    }
}

输出一致:

#include <iostream>

extern "C" typedef void (*Callback)(void *data);

class TestNative
{
public:
    void SetCallback(Callback callback1, void *data);
    void Foo();
private:
    Callback m_callback;
    void *m_data;
};

void TestNative::SetCallback(Callback callback, void * data)
{
    m_callback = callback;
    m_data = data;
}

void TestNative::Foo()
{
    // Foo() will never end
    while (true)
    {
        m_callback(m_data);
    }
}

extern "C"
{
    __declspec(dllexport) TestNative * CreateTestNative()
    {
        return new TestNative();
    }

    __declspec(dllexport) void FreeTestNative(TestNative *obj)
    {
        delete obj;
    }

    __declspec(dllexport) void TestNativeSetCallback(TestNative *obj, Callback callback1, void * data)
    {
        obj->SetCallback(callback1, data);
    }

    __declspec(dllexport) void TestNativeFoo(TestNative *obj)
    {
        obj->Foo();
    }
}

如果取消注释this.Foo() begins TestNet.callback() begins this.callback() this.~TestNet() TestNet.callback() ends TestNet.callback() begins System.NullReferenceException: Object reference not set to an instance of an object. 中的GC.KeepAlive(this)调用,程序将永远不会结束。

1 个答案:

答案 0 :(得分:0)

总结非常有用的评论并完成研究:

1)如果最后一条指令是使用实例拥有的非托管资源的P / Invoke调用,那么在托管实例方法中是否总是需要GC.KeepAlive(this)

是的,如果您不希望API的用户在病理情况下对托管对象的实例承担不可收集的引用的最后责任,请查看以下示例。但这不是唯一的方法:进行P / Invoke Interop时,HandleRefSafeHandle技术也可以用来延长被管理对象的寿命。

该示例随后将通过拥有本地资源的托管实例调用本地方法:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            new Thread(delegate()
            {
                // Run a separate thread enforcing GC collections every second
                while(true)
                {
                    GC.Collect();
                    Thread.Sleep(1000);
                }
            }).Start();

            while (true)
            {
                var test = new TestNet();
                test.Foo();
                TestNet.Dump();
            }
        }
    }

    class TestNet
    {
        static ManualResetEvent _closed;
        static long _closeTime;
        static long _fooEndTime;

        IntPtr _nativeHandle;

        public TestNet()
        {
            _closed = new ManualResetEvent(false);
            _closeTime = -1;
            _fooEndTime = -1;
            _nativeHandle = CreateTestNative();
        }

        public static void Dump()
        {
            // Ensure the now the object will now be garbage collected
            GC.Collect();
            GC.WaitForPendingFinalizers();

            // Wait for current object to be garbage collected
            _closed.WaitOne();
            Trace.Assert(_closeTime != -1);
            Trace.Assert(_fooEndTime != -1);
            if (_closeTime <= _fooEndTime)
                Console.WriteLine("WARN: Finalize() commenced before Foo() return");
            else
                Console.WriteLine("Finalize() commenced after Foo() return");
        }

        ~TestNet()
        {
            _closeTime = Stopwatch.GetTimestamp();
            FreeTestNative(_nativeHandle);
            _closed.Set();
        }

        public void Foo()
        {
            // The native implementation just sleeps for 250ms
            TestNativeFoo(_nativeHandle);

            // Uncomment to have all Finalize() to commence after Foo()
            //GC.KeepAlive(this);
            _fooEndTime = Stopwatch.GetTimestamp();
        }

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern IntPtr CreateTestNative();

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern void FreeTestNative(IntPtr obj);

        [DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]
        static extern void TestNativeFoo(IntPtr obj);
    }
}

为确保本机调用始终安全,我们希望仅在Foo()返回之后调用终结器。相反,我们可以通过在后台线程中手动调用垃圾回收来轻松实施冲突。输出如下:

Finalize() commenced after Foo() return
WARN: Finalize() commenced before Foo() return
Finalize() commenced after Foo() return
Finalize() commenced after Foo() return
Finalize() commenced after Foo() return
WARN: Finalize() commenced before Foo() return
Finalize() commenced after Foo() return

2)在哪里可以找到文档?

GC.KeepAlive()的文档提供了一个与原始问题中的托管回调非常相似的示例。 HandleRef对于托管对象和Interop的生命周期也有非常有趣的考虑:

  

如果使用平台调用来调用托管对象,并且该对象是   在平台调用之后没有在其他地方引用,它是   垃圾收集器最终确定托管对象的可能性。   此操作释放资源并使句柄无效,从而导致   平台调用失败。用HandleRef包装手柄   保证直到   平台调用完成。

由@GSerg找到的链接[1]解释了何时有资格收集对象,并指出this引用不在根集中,从而允许在未返回实例方法时也进行收集。 / p>

3)为什么仅在发布版本中会发生这种情况?

这是一种优化,并且可以在调试版本中启用@SimonMourier指出的启用优化功能。如other answers中所述,默认情况下在Debug中也未启用该功能,因为它可能阻止调试当前方法范围内的变量。

[1] https://devblogs.microsoft.com/oldnewthing/20100810-00/?p=13193