.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();
    }
  }
}

(EOF)

Last updated