首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

Unity 自訂生命週期 PerCallContextLifetimeManager:解決 ThreadPool 重用導致的 PerRequest 偽單例問題

     編輯:ASP.NET 基礎

問題背景:PerThreadLifetimeManager 的陷阱

Unity 內建 PerThreadLifetimeManager 基於 [ThreadStatic] TLS(Thread Local Storage),同一 ManagedThreadId 共用實例。然而 CLR ThreadPool 會重用執行緒:

  1. Request A 在 Thread #10 處理,建立並快取實例 X。
  2. Request A 結束,Thread #10 歸還 ThreadPool。
  3. Request B 分配到 Thread #10,**直接復用實例 X**,導致跨 Request 狀態洩漏(如 DbContext、工作單元、暫存資料)。

這在 ASP.NET Web Forms / MVC / WCF PerCall 託管模式下極為常見,**PerThread ≠ PerRequest**。

解決方案對比

方案可行性缺點
停用 ThreadPool,手動建立/銷毀執行緒理論可行效能災難、擴展性極差、違背 .NET 執行緒模型
每 Request 建立子容器成熟方案需自行管理子容器生命週期(HttpModule/Global.asax)
自訂 PerCallContextLifetimeManager**優雅、零侵入**需理解 CallContext 流動機制

PerCallContextLifetimeManager 實作


using System;
using System.Globalization;
using System.Runtime.Remoting.Messaging;
using Microsoft.Practices.Unity;

public class PerCallContextLifetimeManager : LifetimeManager
{
    private readonly string _key = string.Format(CultureInfo.InvariantCulture,
        "PerCallContextLifetimeManager_{0}", Guid.NewGuid());

    public override object GetValue() => CallContext.LogicalGetData(_key);

    public override void SetValue(object newValue) => CallContext.LogicalSetData(_key, newValue);

    public override void RemoveValue() => CallContext.FreeNamedDataSlot(_key);
}

關鍵點:使用 CallContext.LogicalGetData/SetData 而非 CallContext.GetData/SetData。Logical 版本會隨 ExecutionContext 跨非同步點(await、Task.Run、WCF 處理鏈)流動,完美對齊「邏輯呼叫鏈」邊界。

註冊與使用


container.RegisterType<IExample, Example>(new PerCallContextLifetimeManager());

驗證測試場景


private static void TestPerCallContextLifetimeManager()
{
    IExample example;
    using (var container = new UnityContainer())
    {
        container.RegisterType<IExample, Example>(new PerCallContextLifetimeManager());

        // 同一 CallContext 內多次 Resolve → 同一實例
        container.Resolve<IExample>().SayHello();
        container.Resolve<IExample>().SayHello();

        // 模擬多執行緒併發 Request
        Action act = sleep => {
            container.Resolve<IExample>().SayHello();
            Thread.Sleep(sleep);
            container.Resolve<IExample>().SayHello();
        };

        new Thread(() => act(50)).Start();
        new Thread(() => act(55)).Start();

        // ThreadPool 佇列工作項目(關鍵測試)
        ThreadPool.QueueUserWorkItem(_ => act(50));
        ThreadPool.QueueUserWorkItem(_ => act(55));
        Thread.Sleep(200);

        example = container.Resolve<IExample>();
    }
    // 容器 Dispose 後,外部參考實例仍可用(未被容器銷毀)
    example.SayHello();
    Console.ReadKey();
}

預期結果

  • 每個邏輯呼叫鏈(主執行緒、手動 Thread、ThreadPool 工作項目)**各自擁有獨立實例**。
  • 同一呼叫鏈內多次 Resolve 回傳同一實例。
  • 容器 Dispose 不銷毀實例(行為同 Transient),由 GC 管理。

進階:ASP.NET 整合

在 Global.asax 或 HttpModule 中,於 Application_BeginRequest 建立子容器或標記 CallContext,Application_EndRequest 呼叫 CallContext.FreeNamedDataSlot 清理(雖然 LogicalSetData 隨 ExecutionContext 結束自然失效,顯式清理更保險)。

與 ASP.NET Core 比較

ASP.NET Core 內建 DI 容器提供 ServiceLifetime.Scoped,預設對應 HTTP Request Scope,實作原理同樣基於 AsyncLocal<T>(.NET Core 取代 CallContext)。遷移專案時,PerCallContextLifetimeManager 可視為 Scoped 的 .NET Framework 對應物。

結語

PerCallContextLifetimeManager 以約 20 行程式碼解決 Unity 在 ThreadPool 環境下長期存在的 PerRequest 偽單例問題,零配置、零侵入、相容非同步流程,是維護舊版 .NET Framework + Unity 專案的最佳實踐之一。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved