callbackOnCollectedDelegate MDA
대리자가 함수 포인터로서 관리 코드에서 비관리 코드로 마샬링되고 대리자가 가비지 수집된 이후 해당 함수 포인터에 콜백이 배치되면 callbackOnCollectedDelegate MDA(관리 디버깅 도우미)가 활성화됩니다.
증상
관리 대리자에게서 가져온 함수 포인터를 통해 관리 코드로 호출하려고 시도할 때 액세스 위반이 발생합니다. 이러한 실패는 CLR(공용 언어 런타임) 버그는 아니지만 CLR 코드에 액세스 위반이 발생하기 때문에 그렇게 보일 수 있습니다.
이 문제는 일관되지 않습니다. 경우에 따라 함수 포인터의 호출이 성공하거나 실패할 수 있습니다. 로드가 많거나 임의의 시도 횟수인 경우에만 실패가 발생할 수 있습니다.
원인
함수 포인터가 만들어지고 비관리 코드에 노출된 대리자가 가비지 수집되었습니다. 비관리 구성 요소에서 함수 포인터에 대한 호출을 시도할 때 액세스 위반이 발생합니다.
가비지 수집이 발생하는 시기에 따라 실패도 달라지기 때문에 임의로 발생합니다. 대리자가 수집에 대한 자격이 있을 경우 가비지 수집은 콜백 이후에 발생하고 호출이 성공적으로 수행됩니다. 다른 때는 가비지 수집이 콜백 전에 발생하고 콜백에서 액세스 위반이 발생하며 프로그램이 중지됩니다.
가비지 수집의 횟수와 함께 함수 포인터의 콜백과 대리자 마샬링 사이의 시간에 따라 실패 가능성이 달라집니다. 대리자 마샬링과 콜백 확인 사이의 시간이 짧은 경우 실패는 드물게 발생합니다. 이 경우는 일반적으로 함수 포인터를 받는 관리되지 않는 메서드가 나중에 사용하도록 함수 포인터를 저장하지 않고 대신 함수 포인터에 즉시 콜백하여 작업을 완료한 후 반환하는 경우입니다. 마찬가지로 시스템에 로드가 많으면 가비지 수집이 더 많이 발생하므로 콜백 전에 가비지 수집이 발생합니다.
해결 방법
대리자가 관리되지 않는 함수 포인터로 마샬링된 후에는 가비지 수집기에서 그 수명을 추적할 수 없습니다. 대신 사용자의 코드에 관리되지 않는 함수 포인터의 수명에 대해 대리자의 참조를 보관해야 합니다. 그러나 이 작업을 수행하기 전에 먼저 어떤 대리자를 수집할 것인지 식별해야 합니다. MDA가 활성화되면 해당 대리자의 형식 이름이 제공됩니다. 이 이름을 사용하여 비관리 코드로 대리자를 전달하는 플랫폼 호출 또는 COM 시그니처에 대해 코드를 검색합니다. 잘못 입력된 대리자가 이 호출 사이트 중 하나를 통해 전달됩니다. 또한 gcUnmanagedToManaged MDA를 사용 가능 상태로 만들어 런타임에 대한 모든 콜백 전에 가비지 수집을 강제할 수도 있습니다. 즉, 가비지 수집이 항상 콜백 전에 발생하도록 하여 가비지 수집에서 제시한 불확실성을 제거하게 됩니다. 어떤 대리자가 수집되었는지 알게 되면 코드를 변경하여 마샬링된 관리되지 않는 함수 포인터의 수명에 대해 관리측 대리자의 참조를 보관합니다.
런타임 효과
대리자가 함수 포인터로 마샬링될 때 런타임은 비관리에서 관리로 전환을 수행하는 썽크를 할당합니다. 이 썽크는 관리되는 대리자가 마지막으로 호출되기 전에 실제로 어떤 비관리 코드가 호출되었는지를 나타냅니다. callbackOnCollectedDelegate MDA가 사용 가능 상태가 아닌 경우에는 대리자가 수집될 때 비관리 마샬링 코드가 삭제됩니다. callbackOnCollectedDelegate MDA가 사용 가능 상태인 경우에는 대리자가 수집될 때 비관리 마샬링 코드가 즉시 삭제되지 않습니다. 대신 마지막 1,000개의 인스턴스가 기본적으로 활성화 상태로 유지되며 호출될 때 MDA를 활성화하도록 변경됩니다. 결국 1,001개의 마샬링된 대리자가 수집된 후 썽크가 삭제됩니다.
Output
MDA는 비관리 함수 포인터에서 콜백이 시도되기 전에 수집된 대리자의 형식 이름을 보고합니다.
구성
다음 예제에서는 응용 프로그램 구성 옵션을 보여 줍니다. MDA가 보관한 썽크의 수를 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"); }
}