- 如果托管方法的最后一条指令是对非托管资源的P/Invoke调用,或者在将托管回调从本地代码进行封送时切换到托管执行上下文,那么是否总是需要使用
GC.KeepAlive(this)
?问题可能是:我应该在任何地方都放置GC.KeepAlive(this)
吗?这篇旧的微软博客(原始链接已经失效,这里是缓存)似乎是这样建议的!但这将是一个重大改变,基本上意味着大多数人从未正确地进行过P/Invoke,因为这将要求检查包装器中的大多数P/Invoke调用。例如,是否有规则表明垃圾收集器(编辑:或更好的是终结器)不能在执行上下文为非托管(本机)时运行属于当前线程的对象? - 我可以在哪里找到适当的文档?我找到了CodeAnalysis策略CA2115,指出通常情况下使用
GC.KeepAlive(this)
任何时间使用P/Invoke访问非托管资源。通常情况下,处理终结器时很少需要使用GC.KeepAlive(this)
。 - 为什么只在Release版本中发生这种情况?它看起来像是一种优化,但在Debug版本中根本不需要隐藏垃圾收集器的重要行为。
注意:我不反对委托被收集,那是另一个问题,我知道如何正确处理。这里的问题是当P/Invoke调用尚未完成时,持有非托管资源的对象会被收集。
下面是清晰展示该问题的代码。创建一个C#控制台应用程序和一个C++ Dll1 项目,并以Release模式构建它们:
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();
}
}
输出始终如一:
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.
如果在
TestNet.Foo()
中取消注释GC.KeepAlive(this)
的调用,程序将正确地永远不会结束。
IDisposable
模式,因为我没有持有文件句柄、套接字或其他建议使用IDisposable
的资源:这只是一个 C++ 本地 API,C# 用户不应该被迫依赖于using
。无论如何:我刚刚意识到GC.KeepAlive()
API 正好符合我的回调使用情况,基本上回答了我的第二个问题。你的答案完美地涵盖了问题3)。尚未回答的问题是是否始终需要GC.KeepAlive(this)
。 - ceztko