callbackOnCollectedDelegate MDA
更新:2007 年 11 月
如果将一个委托作为函数指针从托管代码封送到非托管代码,并且在对该委托进行垃圾回收后对该函数指针发出了一个回调,则将激活 callbackOnCollectedDelegate 托管调试助手 (MDA)。
症状
试图通过从托管委托获取的函数指针调入托管代码时发生访问冲突。这些故障虽然不是公共语言运行库 (CLR) bug,但是会显示为公共语言运行库 bug,因为 CLR 代码中发生了访问冲突。
此故障的具体情况并非千篇一律:有时对函数指针发出的调用可以成功,有时却会失败。仅在负载过大或按随机次数进行尝试时会发生此故障。
原因
从其创建函数指针并将创建的函数指针公开给非托管代码的委托已被垃圾回收。当非托管组件尝试对该函数指针发出调用时,会产生访问冲突。
该故障是随机出现的,因为它的出现取决于垃圾回收发生的时间。如果某个委托符合回收条件,则会在回调之后进行垃圾回收,并且调用会成功。在其他一些情况下,垃圾回收发生在回调之前,回调会引发访问冲突,并且程序会停止。
该故障发生的可能性取决于从封送委托到对函数指针发出回调之间的时间以及垃圾回收的频率。如果从封送委托到接着发生的回调之间的时间很短,则发生该故障会的机会就很少。通常情况下,如果接收函数指针的非托管方法没有保存该函数指针以备以后使用,而是立即对函数指针发出回调以便在返回之前完成其操作,就很少发生这种故障。同样道理,系统负载过大时会发生更多垃圾回收,进而使得垃圾回收更有可能在回调之前发生。
解决办法
一旦将委托作为非托管函数指针封送出去,垃圾回收器就无法跟踪其生存期。这样,在该非托管函数指针的生存内,您的代码必须保持一个指向该委托的引用。但是在此之前,您首先必须确定回收了哪个委托。激活 MDA 之后,MDA 会提供该委托的类型名称。请使用此名称在您的代码中搜索将该委托外传给非托管代码的平台调用或 COM 签名。通过这些调用站点之一将有问题的委托传递出去。您还可以启用 gcUnmanagedToManaged MDA 以强制在每次向运行库发出回调之前都进行垃圾回收。这样可以确保在回调之前总是进行垃圾回,从而可以消除由垃圾回收引起的不确定性。一旦您得知回收了哪个委托,请更改您的代码,以便在封送的非托管函数指针的生存期内在托管端保持对该委托的引用。
对运行库的影响
将委托作为函数指针进行封送时,运行库会分配一个 thunk,用于执行从非托管到托管的转换。此 thunk 是指最终调用托管委托之前非托管代码实际调用的内容。如果不启用 callbackOnCollectedDelegate MDA,则会在回收委托时删除非托管封送代码。如果启用 callbackOnCollectedDelegate MDA,则在回收委托时不会立即删除非托管封送代码。在默认情况下,最后 1,000 个实例将保持活动状态,并更改为在被调用时激活此 MDA。在回收另外 1,001 个封送委托之后,最终将删除此 thunk。
输出
MDA 会报告在试图对其非托管函数指针发出回调之前回收的委托的类型名称。
配置
下面的示例演示应用程序配置选项。该示例将 MDA 保持为活动状态的 thunk 的数量设置为 1,500。默认 listSize 值为 1,000,最小值为 50,最大值为 2,000。
<mdaConfig>
<assistants>
<callbackOnCollectedDelegate listSize="1500" />
</assistants>
</mdaConfig>
示例
下面的示例演示一种可激活此 MDA 的情况:
// Library.cpp : Defines the unmanaged entry point for the DLL application.
#include "windows.h"
#include "stdio.h"
void (__stdcall *g_pfTarget)();
void __stdcall Initialize(void __stdcall pfTarget())
{
g_pfTarget = pfTarget;
}
void __stdcall Callback()
{
g_pfTarget();
}
// ---------------------------------------------------
// C# Client
using System;
using System.Runtime.InteropServices;
public class Entry
{
public delegate void DCallback();
public static void Main()
{
new Entry();
Initialize(Target);
GC.Collect();
GC.WaitForPendingFinalizers();
Callback();
}
public static void Target()
{
}
[DllImport("Library", CallingConvention = CallingConvention.StdCall)]
public static extern void Initialize(DCallback pfDelegate);
[DllImport ("Library", CallingConvention = CallingConvention.StdCall)]
public static extern void Callback();
~Entry() { Console.Error.WriteLine("Entry Collected"); }
}