callbackOnCollectedDelegate MDA
如果將委派 (Delegate) 從 Managed 程式碼封送處理 (Marshal) 至 Unmanaged 程式碼做為函式指標,並在對委派進行記憶體回收之後,將回呼 (Callback) 置於該函式指標,callbackOnCollectedDelegate Managed 偵錯助理 (MDA) 就會啟動。
症狀
嘗試透過從 Managed 委派取得的函式指標,呼叫到 Managed 程式碼時,發生存取違規。 這些失敗儘管不是 Common Language Runtime (CLR) 錯誤,卻可能由於 CLR 程式碼中發生的存取違規,看來像是 CLR 錯誤。
這個失敗並非一直發生,有時候對函式指標的呼叫會成功,有時候則會失敗。 此失敗可能只會在負載過重或進行隨機嘗試的情況下發生。
原因
從中建立函式指標並將它公開至 Unmanaged 程式碼的委派,遭到記憶體回收。 當 Unmanaged 元件嘗試呼叫函式指標時,就會產生存取違規。
失敗可能會因發生記憶體回收而隨機出現。 如果委派適合回收,在回呼和呼叫成功之後可能會發生記憶體回收。 在其他時候,記憶體回收會在回呼之前發生,回呼會產生存取違規,而程式則會停止。
失敗的可能性,需視封送處理委派與在函式指標回呼之間的時間,以及記憶體回收的頻率而定。 如果封送處理委派與接下來回呼之間的時間相當短暫,便會偶爾發生失敗。 這通常發生於,接收函式指標的 Unmanaged 方法,沒有儲存函式指標以便在稍後使用,而是立即在函式指標回呼,以便在傳回之前完成它的作業等情況中。 同樣地,當系統負載過重時,會發生更多記憶體回收,這讓記憶體回收更有可能在回呼之前發生。
解決方式
一旦將委派封送處理為 Unmanaged 函式指標,記憶體回收就會無法追蹤委派的存留期 (Lifetime)。 相反地,您的程式碼必須維持對委派的參考,才能追蹤 Unmanaged 函式指標的存留期。 不過在您開始這樣做之前,必須先識別出已回收哪個委派。 當 MDA 啟動時,會提供委派的型別名稱。 請使用這個名稱搜尋您的程式碼,以找出將該委派傳遞到 Unmanaged 程式碼的平台叫用或 COM 簽章 (Signature)。 違規的委派就是透過這些其中一個呼叫位置傳遞出去的。 您也可以啟用 gcUnmanagedToManaged MDA,以便在每次向執行階段回呼之前,先強制執行記憶體回收。 藉著確定記憶體回收一定會在回呼之前發生,就能消除記憶體回收所帶來的不確定性。 一旦知道回收了哪個委派之後,請變更您的程式碼,維持對 Mananged 端上該委派的參考,以便追蹤封送處理之 Unmanaged 函式指標的存留期。
對執行階段的影響
當委派封送處理為函式指標時,執行階段就會配置 Thunk 以進行從 Unmanaged 至 Managed 的轉換。 這個 Thunk 也就是在最後叫用 Mananged 委派之前,Unmanaged 程式碼確實呼叫的對象。 在沒有啟用 callbackOnCollectedDelegate MDA 的情況下,便會在回收委派時刪除 Unmanaged 封送處理程式碼。 如果啟用了 callbackOnCollectedDelegate MDA,則不會在回收委派時立即刪除封送處理程式碼, 而是根據預設,保留最後 1,000 個執行個體的運作,直到呼叫時才會變更為啟動 MDA。 在回收了 1,001 個以上的封送處理委派之後,最後才會刪除這個 Thunk。
Output
這個 MDA 會報告在它的 Unmanaged 函式指標嘗試回呼之前,所回收之委派的型別名稱。
組態
下列範例顯示應用程式組態選項。 此範例會將 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"); }
}