在非托管资源上执行 P/Invoke 时,何时需要 GC.KeepAlive(this)? [英] When GC.KeepAlive(this) is needed when doing P/Invoke on unmanaged resources?

查看:19
本文介绍了在非托管资源上执行 P/Invoke 时,何时需要 GC.KeepAlive(this)?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

  1. 如果最后一条指令是对非托管资源的 P/Invoke 调用,或者在这种特殊情况下才需要,则托管方法中总是需要 GC.KeepAlive(this),即切换到托管执行从本机代码封送托管回调时的上下文?问题可能是:我应该把 GC.KeepAlive(this) 放在任何地方吗?这个老微软 博客(原始链接是 404,这里是 cached) 似乎表明如此!但这将改变游戏规则,基本上这意味着大多数人从未正确执行 P/Invoke,因为这需要检查包装器中的大多数 P/Invoke 调用.例如,是否有一条规则说垃圾收集器(编辑:或更好的终结器)不能在执行上下文不受管理(本机)时为属于当前线程的对象运行?
  2. 在哪里可以找到合适的文档?我可以找到 CodeAnalysis 政策 CA2115 指向一般使用 GC.KeepAlive(this) 任何 时间使用 P/Invoke 访问非托管资源.一般来说,在处理 终结器.
  3. 为什么这只会在发布版本中发生?它看起来像是一种优化,但在 Debug 构建中根本不需要,这隐藏了垃圾收集器的一个重要行为.

注意:我对收集代表没有问题,这是一个不同的问题,我知道如何正确处理.这里的问题是当 P/Invoke 调用尚未完成时,持有非托管资源的对象会被收集.

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

Program.cs:

使用系统;使用 System.Runtime.InteropServices;命名空间 ConsoleApp1{课程计划{静态无效主(字符串 [] args){var test = new TestNet();尝试{测试.Foo();}捕获(异常前){Console.WriteLine(ex);}}}类测试网{[UnmanagedFunctionPointer(CallingConvention.Cdecl)]委托无效回调(IntPtr数据);静态回调_callback;IntPtr _nativeHandle;GCHandle _thisHandle;静态测试网(){//注意:保留委托引用,以便它们可以//持久存储在非托管资源中_callback = 回调;}公共测试网(){_nativeHandle = CreateTestNative();//保持对 self 的弱句柄.弱是必须的//不阻止 TestNet 实例的垃圾收集_thisHandle = GCHandle.Alloc(this, GCHandleType.Weak);TestNativeSetCallback(_nativeHandle, _callback, GCHandle.ToIntPtr(_thisHandle));}~测试网(){Console.WriteLine("this.~TestNet()");FreeTestNative(_nativeHandle);_thisHandle.Free();}公共无效 Foo(){Console.WriteLine("this.Foo() 开始");TestNativeFoo(_nativeHandle);//收集对象时永远不会打印!Console.WriteLine("this.Foo() 结束");//没有下面的 GC.KeepAlive(this) 调用//在发布版本中,程序将始终收集//callback() 中的对象并在下一次迭代时崩溃//GC.KeepAlive(this);}静态无效回调(IntPtr 数据){Console.WriteLine("TestNet.callback() 开始");//检索对 self 的弱引用.等一等//TestNet 存在.var self = (TestNet)GCHandle.FromIntPtr(data).Target;self.callback();//强制垃圾回收.在发布版本中自我=空;GC.Collect();GC.WaitForPendingFinalizers();Console.WriteLine("TestNet.callback() 结束");}无效回调(){Console.WriteLine("this.callback()");}[DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]静态外部 IntPtr CreateTestNative();[DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]静态外部无效 FreeTestNative(IntPtr obj);[DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]static extern void TestNativeSetCallback(IntPtr obj, Callback callback, IntPtr data);[DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]静态外部无效 TestNativeFoo(IntPtr obj);}}

Dll1.cpp:

#include extern "C" typedef void (*Callback)(void *data);类 TestNative{上市:void SetCallback(Callback callback1, void *data);空 Foo();私人的:回调 m_callback;无效 *m_data;};void TestNative::SetCallback(回调回调,void * data){m_callback = 回调;m_data = 数据;}void TestNative::Foo(){//Foo() 永远不会结束而(真){m_callback(m_data);}}外部C"{__declspec(dllexport) TestNative * CreateTestNative(){返回新的 TestNative();}__declspec(dllexport) void FreeTestNative(TestNative *obj){删除对象;}__declspec(dllexport) void TestNativeSetCallback(TestNative *obj, Callback callback1, void * data){obj->SetCallback(callback1, data);}__declspec(dllexport) void TestNativeFoo(TestNative *obj){对象-> Foo();}}

输出一致:

this.Foo() 开始TestNet.callback() 开始this.callback()这个.~测试网()TestNet.callback() 结束TestNet.callback() 开始System.NullReferenceException:未将对象引用设置为对象的实例.

如果取消注释 TestNet.Foo() 中的 GC.KeepAlive(this) 调用,程序将永远不会结束.

解决方案

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

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

是的,如果您不希望 API 的用户承担在病理情况下持有托管对象实例的不可收集引用的最后责任,请查看下面的示例.但这不是唯一的方法:HandleRefSafeHandle 在执行 P/Invoke Interop 时,还可以使用技术来延长托管对象的生命周期.

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

使用系统;使用 System.Diagnostics;使用 System.Runtime.InteropServices;使用 System.Threading;命名空间 ConsoleApp1{课程计划{静态无效主(字符串 [] args){新线程(委托(){//每秒运行一个单独的线程来强制执行 GC 收集同时(真){GC.Collect();线程睡眠(1000);}}).开始();而(真){var test = new TestNet();测试.Foo();TestNet.Dump();}}}类测试网{静态 ManualResetEvent _closed;静态长_closeTime;静态长_fooEndTime;IntPtr _nativeHandle;公共测试网(){_closed = new ManualResetEvent(false);_closeTime = -1;_fooEndTime = -1;_nativeHandle = CreateTestNative();}公共静态无效转储(){//确保现在对象将被垃圾收集GC.Collect();GC.WaitForPendingFinalizers();//等待当前对象被垃圾回收_closed.WaitOne();Trace.Assert(_closeTime != -1);Trace.Assert(_fooEndTime != -1);如果(_closeTime <= _fooEndTime)Console.WriteLine("WARN: Finalize() 在 Foo() 返回之前开始");别的Console.WriteLine("Finalize() 在 Foo() 返回后开始");}~测试网(){_closeTime = Stopwatch.GetTimestamp();FreeTestNative(_nativeHandle);_closed.Set();}公共无效 Foo(){//本机实现只休眠 250 毫秒TestNativeFoo(_nativeHandle);//取消注释以在 Foo() 之后开始所有 Finalize()//GC.KeepAlive(this);_fooEndTime = Stopwatch.GetTimestamp();}[DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]静态外部 IntPtr CreateTestNative();[DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]静态外部无效 FreeTestNative(IntPtr obj);[DllImport("Dll1", CallingConvention = CallingConvention.Cdecl)]静态外部无效 TestNativeFoo(IntPtr obj);}}

为了使本机调用始终安全,我们希望仅在 Foo() 返回后才调用终结器.相反,我们可以通过在后台线程中手动调用垃圾收集来轻松地强制执行违规.输出如下:

Finalize() 在 Foo() 返回后开始警告:Finalize() 在 Foo() 返回之前开始Finalize() 在 Foo() 返回后开始Finalize() 在 Foo() 返回后开始Finalize() 在 Foo() 返回后开始警告:Finalize() 在 Foo() 返回之前开始Finalize() 在 Foo() 返回后开始

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

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

<块引用>

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

@GSerg 找到的链接 [1] 还解释了对象何时符合收集条件,指出 this 引用不在根集中,允许在实例方法没有时也收集它返回.

3) 为什么这只会在发布版本中发生?

正如@SimonMourier 指出的那样,这是一种优化,也可以在调试构建中发生,并启用优化.默认情况下,Debug 中也未启用它,因为它可能会阻止调试当前方法范围内的变量,如这些 其他 答案.>

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

I have a TestNet wrapper for a native component. The native component exposes a blocking TestNative::Foo() that communicates with managed part through calling managed callbacks and a weak GCHandle that is used to retrieve the reference to the .NET wrapper and provides a context. The GCHandle is weak since the .NET wrapper is meant to hide the fact that is handling unmanaged resources to user and deliberately doesn't implement the IDisposable interface: being non weak it would prevent TestNet instances from being collected at all, creating a memory leak. What's happening is that in Release build only the garbage collector will collect reference to .NET wrapper while executing the managed callback, even before both TestNative::Foo() and surprisingly TestNet::Foo() unblocks. I understood the problem my self and I can fix it by issuing a GC.KeepAlive(this) after the P/Invoke call but since the knowledge of this is not very widespread, it seems a lot of people are doing it wrong. I have few questions:

  1. Is GC.KeepAlive(this) always needed in a managed method if last instruction is a P/Invoke call on unmanaged resources or it's just needed in this special case, namely the switch to managed execution context while marshaling the managed callback from native code? The question could be: should I put GC.KeepAlive(this) everywhere? This old microsoft blog (original link is 404, here is cached) seems to suggest so! But this would be game changer and basically it would mean that most people never did P/Invoke correctly, because this would require reviewing most P/Invoke calls in wrappers. Is there for example a rule that say that garbage collector (EDIT: or better the finalizer) can't run for objects that belong to the current thread while execution context is unamanaged (native)?
  2. Where I can find proper documentation? I could find CodeAnalysis policy CA2115 pointing to generically use GC.KeepAlive(this) any time a unmanaged resource is accessed with P/Invoke. In general GC.KeepAlive(this) seems to be very rarely needed when dealing with finalizers.
  3. Why is this happening only in Release build? It looks like an optimization but not being needed at all in Debug build hides an important behavior of the garbage collector.

NOTE: I have no problem with delegates being collected, that is a different issue which I know how to handle properly. The issue here is with objects holding unmanaged resources being collected when P/Invoke calls are not finished yet.

It follows code that clearly manifest the problem. Creates a C# console application and a C++ Dll1 project and build them in Release mode:

Program.cs:

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);
    }
}

Dll1.cpp:

#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();
    }
}

The output is consistently:

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.

If one uncomment the GC.KeepAlive(this) call in TestNet.Foo() the program correctly never ends.

解决方案

Summarizing very useful comments and research done:

1) Is GC.KeepAlive(this) always needed in a managed instance method if last instruction is a P/Invoke call using unmanaged resources hold by the instance?

Yes, if you don't want the user of the API to have last responsibility of holding a non-collectible reference for the instance of the managed object in pathological cases, look the example below. But it's not the only way: HandleRef or SafeHandle techiniques can also be used to prolong the lifetime of a managed object when doing P/Invoke Interop.

The example will subsequently call native methods through managed instances holding native resources:

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);
    }
}

For the native call to be always safe we expect finalizer to be called only after Foo() return. Instead we can easily enforce violations by manually invoking garbage collection in a background thread. Output follows:

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) Where I can find documentation?

Documentation of GC.KeepAlive() provides an example very similar to the managed callback in the original question. HandleRef has also very interesting considerations about lifecycle of managed objects and Interop:

If you use platform invoke to call a managed object, and the object is not referenced elsewhere after the platform invoke call, it is possible for the garbage collector to finalize the managed object. This action releases the resource and invalidates the handle, causing the platform invoke call to fail. Wrapping a handle with HandleRef guarantees that the managed object is not garbage collected until the platform invoke call completes.

Also link[1] found by @GSerg explains when an object is eligible for collection, pointing that this reference is not in the root set, allowing it to be collected also when instance method has not returned.

3) Why is this happening only in Release build?

It's an optimization and can happen also in Debug build, with optimization enabled, as pointed by @SimonMourier. It's not enabled by default also in Debug because it could prevent debugging of variables in the current method scope, as explained in these other answers.

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

这篇关于在非托管资源上执行 P/Invoke 时,何时需要 GC.KeepAlive(this)?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆