C#互操作之GC回收陷阱

14 篇文章 0 订阅
7 篇文章 0 订阅

起因

其实这得从好多年前的一个BUG说起.

那一年,  刚刚接触C++不久, 遇到的一个空引用之类的错误,反复调试却没有发现C++有任何的问题

 单独跑C#测试也没有任何问题,  屏蔽C++的回调才找到出问题的地方。

示例代码

为了复现那个BUG的样子,我甩个DEMO出来。

C++的代码是下面这样的,公布SetCallback函数,由C#设置回调地址,然后在C++非托管线程中不断调用该回调

 

/// 数据回调申明
typedef void (WINAPI *DataCallback)(int nData);


#ifdef __cplusplus
extern "C" 
{
#endif 
#define  CDLLINVOKE_EXPORTS __declspec(dllexport)


	CDLLINVOKE_EXPORTS void SetCallback(DataCallback pPt);
	 
#ifdef __cplusplus
}
#endif 


DataCallback m_pCallback = NULL;
/// 
/// 产生数据					   
/// 
DWORD WINAPI GenerateData(PVOID pParam)
{
	int nCnt = 0;
	while (true) 
	{
		Sleep(20);
		if(NULL!= m_pCallback)
		{
			m_pCallback(nCnt);
		}
		nCnt ++;
	}
	return 0;
}
/// 
/// 设置数据回调					   
/// 
CDLLINVOKE_EXPORTS void SetCallback(DataCallback pPt)
{
	m_pCallback = pPt;
	CreateThread(NULL, 0, GenerateData, NULL, 0, NULL);

}

 

 

 

 

 

C#代码是下面这样的。通过对CDllInvoke.dll的互操作设置回调地址,然后将非托管的回调数据打印出来。

 

namespace ConsoleApplication1
{
    class CDllInvoke
    {
        const string DllName = "CDllInvoke.dll";

        [UnmanagedFunctionPointer(System.Runtime.InteropServices.CallingConvention.StdCall)]
        delegate void DataCallback(int nData);
        [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
        static extern void SetCallback(DataCallback pCall);
        void Callback(int nData)
        {
            Console.WriteLine("收到回调:{0}", nData);
        } 
        public void Run()
        {
            SetCallback(Callback); 
        }
      
    }
    class Program
    {

        static void Main(string[] args)
        {
            CDllInvoke test = new CDllInvoke();
            test.Run();
            //GC.Collect() 模拟GC自动回收。

while (true) { Thread.Sleep(100); } } } }
生成C++代码为CDllInvoke.dll ,生成C#代码为exe执行程序。然后执行exe。

 

异常发生

当然,这个程序并不一定会出现异常。  为了加快异常发生。各位可在C#代码test.Run()后面那行 注释取消。GC.Collect()  

 结果执行完后程序立即就崩溃了,相信看到这里,大家已经明白我的意思了。

 大胆假设

很明显这个问题与GC回收有关

我们知道,在编译 SetCallback(Callback); 这句话的时候,编译器会自动创建一个代理。也就是说上面这句代码与下面这两句,对编译器来讲是没有什么区别的

 

  DataCallback pCall = new DataCallback(Callback); 
  SetCallback(pCall); 

 

而实例pCall在set过后就设置到了非托管代码,GC并不知道该引用的存在,判断到引用计数器为0,于是就释放掉了这个实例。

而在C++回调处,还把它当成一个正常的函数指针调用,最后导致了异常的发生。

小心求证

空口无凭,我们可以通过查看编译后IL代码证明我的假设(不知道IL的看这里

这里选择通过VS2010自带的工具,IL 反汇编程序反编译。(该工具可在开始菜单->Microsoft Visual Studio 2010 目录下找到,对了,我是假设你安装了VS的的)

Run方法对应的 IL代码【在Release编译后用IL反编译】

版本一:创建代理的实例,然后赋值

 

public void Run()
{
 DataCallback pCall = new DataCallback(Callback);
 SetCallback(pCall); 
}
.method public hidebysig instance void  Run() cil managed
{
  // 代码大小       20 (0x14)
  .maxstack  3
  .locals init ([0] class ConsoleApplication1.CDllInvoke/DataCallback pCall)
  IL_0000:  ldarg.0
  IL_0001:  ldftn      instance void ConsoleApplication1.CDllInvoke::Callback(int32)
  IL_0007:  newobj     instance void ConsoleApplication1.CDllInvoke/DataCallback::.ctor(object,
                                                                                        native int)
  IL_000c:  stloc.0
  IL_000d:  ldloc.0
  IL_000e:  call       void ConsoleApplication1.CDllInvoke::SetCallback(class ConsoleApplication1.CDllInvoke/DataCallback)
  IL_0013:  ret
} // end of method CDllInvoke::Run


版本二:直接使用语法糖,设置方法地址

 

public void Run()
{ 
  SetCallback(Callback); 
}		
 .method public hidebysig instance void  Run() cil managed
{
  // 代码大小       18 (0x12)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldftn      instance void ConsoleApplication1.CDllInvoke::Callback(int32)
  IL_0007:  newobj     instance void ConsoleApplication1.CDllInvoke/DataCallback::.ctor(object,
                                                                                        native int)
  IL_000c:  call       void ConsoleApplication1.CDllInvoke::SetCallback(class ConsoleApplication1.CDllInvoke/DataCallback)
  IL_0011:  ret
} // end of method CDllInvoke::Run


  事实证明,两个版本的IL代码还是有一些不同的 (不要问我为什么看得懂IL代码,我也是现学现用):
版本一的IL代码中,还是有一个局部变量pCall的存在;而在版本二中,是不存在该局部变量的。
尽管有这个区别,两个版本却都使用了newobj 创建了一个实例,版本一将实例赋值给局部变量,版本二将实例保存在堆栈。

 

 

所以, 虽然我前面的推测不太准确,但是区别并不大。

两个版本的程序执行都会发生同样的错误,而出错的直接原因均是局部变量被GC回收。

解决方法

知道原因,解决就不难了,既然是局部变量被回收,那就延长变量的生命周期。

 

版本三:延长代理实例的生命周期,解决回收的问题

 class CDllInvoke
    {
        const string DllName = "CDllInvoke.dll";

        [UnmanagedFunctionPointer(System.Runtime.InteropServices.CallingConvention.StdCall)]
        delegate void DataCallback(int nData);
        [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)]
        static extern void SetCallback(DataCallback pCall);
        void Callback(int nData)
        {
            Console.WriteLine("收到回调:{0}", nData);
        }
        DataCallback pCall;
        public void Run()
        {
            pCall = new DataCallback(Callback);
            SetCallback(pCall); 
        } 
    }
.method public hidebysig instance void  Run() cil managed
{
  // 代码大小       30 (0x1e)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldarg.0
  IL_0002:  ldftn      instance void ConsoleApplication1.CDllInvoke::Callback(int32)
  IL_0008:  newobj     instance void ConsoleApplication1.CDllInvoke/DataCallback::.ctor(object,
                                                                                        native int)
  IL_000d:  stfld      class ConsoleApplication1.CDllInvoke/DataCallback ConsoleApplication1.CDllInvoke::pCall
  IL_0012:  ldarg.0
  IL_0013:  ldfld      class ConsoleApplication1.CDllInvoke/DataCallback ConsoleApplication1.CDllInvoke::pCall
  IL_0018:  call       void ConsoleApplication1.CDllInvoke::SetCallback(class ConsoleApplication1.CDllInvoke/DataCallback)
  IL_001d:  ret
} // end of method CDllInvoke::Run

顺便贴出了最终版本的Run方法IL反汇编代码,各位感受下。

 

 

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值