.NET6 之 DLL 熱插拔(AssemblyLoadContext)紀錄
let DLL hot swap. 動態卸載。AssemblyLoadContext。
引言
讓 DLL 檔熱插拔。
若只要動態載入 DLL,用 Assembly.LoadFrom()
指令即可,不過載入後就歸入 AppDomain
且無法卸載直到程式結束才會卸載。
若要可以動態卸載(熱插拔)就要用 AssemblyLoadContext 類別來載入,且 isCollectible
屬性一定要設成 true。還要搭配 WeakReference 類別才能真的卸載。還有一些微妙的操作才能有作用。
應用上可用來實作系統的外部插件(plug-in)或 debug 工具程式。
古早以前用 Loadlibrary / FreeLibrary 指令就能搞定了,改成這樣不知是進步還是…
參考文件
進階參考文件(未認證)
關鍵原碼紀錄
依官方標準解,一定要繼承 AssemblyLoadContext 。記得設定 isCollectible = true
,才能動態卸載。
/// <summary>
/// ExecuteAndUnload
/// ref→[How to use and debug assembly unloadability in .NET](https://learn.microsoft.com/en-us/dotnet/standard/assembly/unloadability)
/// </summary>
sealed class CollectibleAssemblyLoadContext : AssemblyLoadContext
{
public CollectibleAssemblyLoadContext(string name) : base(name, isCollectible: true)
{
//※ 記得設定 isCollectible = true ,才能動態卸載。
}
protected override Assembly? Load(AssemblyName name)
{
return null;
}
}
一定要有一個 ExecuteAndUnload
函式且要加掛 [MethodImpl(MethodImplOptions.NoInlining)]
屬性以避免被C# 編輯成 inline 模式讓動態卸載無效。
[MethodImpl(MethodImplOptions.NoInlining)]
async Task<(JobResult, WeakReference)> ExecuteAndUnload2Async(TaskQueue runTask, JobMaster job, JobDetail jobStep, CancellationToken cancelToken)
{
//# 為每個 job (或插件)建立獨立的 "Domain"。
var alc = new CollectibleAssemblyLoadContext($"YourJobService_{job.JobId}");
WeakReference alcWeakRef = new WeakReference(alc, trackResurrection: true);
//-------------------------------------------------------------------------
try
{
//## 動態載入模組
//Assembly jobAsm = Assembly.LoadFrom(jobStep.TypeAsmPath);
Assembly jobAsm = alc.LoadFromAssemblyPath(jobStep.TypeAsmPath);
_logger.LogDebug($"動態載入模組:{jobStep.TypeAsmPath}。");
//## 執行排程工作
JobResult result = await InvokeJobStepAsync(jobAsm, job, jobStep, runTask, cancelToken);
return (result, alcWeakRef);
}
finally
{
//-------------------------------------------------------------------------
//# 啟動卸載(其實只是標記或宣示已『Unload』)
string jobDllFiles = String.Join(", ", alc.Assemblies.Select(asm => asm.FullName).Distinct());
_logger.LogInformation($"啟動卸載 => {jobDllFiles}。");
alc.Unload(); // 此指令只是標記卸載,該模組其實大概還在記憶體內。
}
}
該 DLL 的任務完成後(或插件程序執行完後)用 WeakReference 類別註記該 DLL 已『Unload』。 並在最後的最後用 GC.Collect 指令真的釋放它。
/// 組建並執行排程工作
async ValueTask BuildWorkItemAsync(TaskQueue runTask, CancellationToken cancelToken)
{
List<WeakReference> alcWeakRefList = new();
//※用 WeakReference 類別來追踨(標記)某DLL已『Unload』。
try
{
//# 依步驟順序執行
foreach (var step in jobStepList.OrderBy(c => c.StepSeq))
{
(JobResult result, WeakReference alcWeakRef) = await ExecuteAndUnload2Async(runTask, job, step, cancelToken);
alcWeakRefList.Add(alcWeakRef);
//※ 用 WeakReference 類別註記該 DLL 已『Unload』
if(result.code == JobResultCode.SUCCESS)
...略...
}
//# 執行完成...略...
}
catch (Exception ex)
{
string errMsg = ExtractExceptionMessage(ex);
_logger.LogCritical($"BuildWorkItem: EXCEPTION => {errMsg}");
}
finally
{
//# 最後的最後用 GC.Collect 真的釋放DLL。
int round = 0;
for (; alcWeakRefList.Any(alc => alc.IsAlive) && (round < 10); round++)
{
await Task.Yield();
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
}
以上程序因是後來加入的新需求故寫的不好。應該可以整合成一個類別執行用來執行 DLL 任務,完成任務後就『Unolad』它。不過 WeakReference 也是要放在整體流程的最後的最後才用 GC.Collect 放掉它。
(EOF)
Last updated