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

CSLA 框架服務端多線程實戰:從單線程瓶頸到高併發解決方案

     編輯:關於.NET

在前一篇文章中,我們探討了如何利用非同步線程實現資料預載入,以提升系統效能。這類操作通常在客戶端執行,旨在縮短使用者等待時間。然而,若客戶端發送多個非同步請求,而服務端不支援多線程處理,而是線性逐一處理請求,則客戶端的非同步設計將失去意義。

雖然現代服務端架構理應支援多線程,但我們的系統採用 CSLA 作為分散式框架,其服務端預設僅支援單線程模式。經查閱 CSLA 官方論壇,框架作者指出因 GlobalContext 與 ClientContext 的限制,暫不支援多線程。由於系統已深度依賴 CSLA,短期內難以替換框架,因此我們決定深入原始碼進行修改。

一、修改 WCF 通訊類別:啟用服務端多線程

.NET 3.5 版 CSLA 使用 WCF 實現資料傳輸,服務端透過 WcfPortal 類別接收請求:

namespace Csla.Server.Hosts
{
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class WcfPortal : IWcfPortal { }
}

該類別未標註 ConcurrencyMode = ConcurrencyMode.Multiple 與 UseSynchronizationContext = false,故預設為單線程模式。我們採用裝飾模式建立新類別,避免修改原始碼:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall,
    ConcurrencyMode = ConcurrencyMode.Multiple,
    UseSynchronizationContext = false)]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class MultiThreadsWCFPortal : IWcfPortal
{
    private WcfPortal _innerPortal = new WcfPortal();

    #region IWcfPortal Members
    public WcfResponse Create(CreateRequest request)
    {
        return this._innerPortal.Create(request);
    }
    // 其他介面方法同理委派...
    #endregion
}

接著需在兩處替換類別實例化:

1. 組態檔 (app.config)

<services>
    <service name="OpenExpressApp.Server.WPFHost.MultiThreadsWCFPortal" behaviorConfiguration="returnFaults">
        .....
    </service>
</services>

2. Factory 方法

private static Type GetServerHostType()
{
    return typeof(OpenExpressApp.Server.WPFHost.MultiThreadsWCFPortal);
    // return typeof(Csla.Server.Hosts.WcfPortal);
}

如此修改後,服務端接收請求時將自動啟動多線程回應,且裝飾模式確保原始碼零修改。

二、修正 ApplicationContext._principal 靜態欄位導致的執行緒安全問題

啟用 WCF 層級多線程後,執行應用程式卻拋出 NullReferenceException。除錯發現 Csla.ApplicationContext.User 為 UnauthenticatedIdentity,即便使用者已登入。追蹤原始碼發現,CSLA 在請求處理初期會將此屬性設為客戶端傳入的使用者身分,但其實作存在缺陷:

private static IPrincipal _principal;
public static IPrincipal User
{
    get
    {
        IPrincipal current;
        if (HttpContext.Current != null)
            current = HttpContext.Current.User;
        else if (System.Windows.Application.Current != null)
        {
            if (_principal == null)
            {
                if (ApplicationContext.AuthenticationType != "Windows")
                    _principal = new Csla.Security.UnauthenticatedPrincipal();
                else
                    _principal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
            }
            current = _principal;
        }
        else
            current = Thread.CurrentPrincipal;
        return current;
    }
    set
    {
        if (HttpContext.Current != null)
            HttpContext.Current.User = value;
        else if (System.Windows.Application.Current != null)
            _principal = value;
        Thread.CurrentPrincipal = value;
    }
}

程式碼顯示:若服務端為 WPF 應用程式,則使用靜態欄位 _principal 儲存目前使用者。這意味著所有執行緒共用同一個靜態變數,導致多線程環境下只能取得最後一個請求的使用者資訊。作者誤以為 WPF 僅用於客戶端,未考慮服務端同樣採用 WPF 的情況。

由於此類別同時服務於客戶端與服務端,修改必須不影響客戶端正常運作。我們引入 [ThreadStatic] 屬性,區分執行位置:

[ThreadStatic]
private static IPrincipal __principalThreadSafe;
private static IPrincipal __principal;

private static IPrincipal _principal
{
    get
    {
        return _executionLocation == ExecutionLocations.Client ? __principal : __principalThreadSafe;
    }
    set
    {
        if (_executionLocation == ExecutionLocations.Client)
        {
            __principal = value;
        }
        else
        {
            __principalThreadSafe = value;
        }
    }
}

此修改確保服務端每個執行緒擁有獨立的 Principal,解決多線程下的身分衝突。

三、手動開啟的執行緒:傳遞 Principal 的包裝器模式

解決上述兩點後,WCF 處理請求的執行緒已能正確存取 ApplicationContext.User。但在單一請求處理過程中,若我們手動啟動額外執行緒(如非同步作業),這些新執行緒的 ApplicationContext.User 未被 CSLA 賦值,同樣會導致異常。

我們的非同步處理程式碼經過封裝,因此可在啟動執行緒時加入「包裝器」,確保新執行緒繼承呼叫者的 Principal:

public static void SafeInvoke(Action action)
{
    action = action.AsynPrincipleWrapper();
    ThreadPool.QueueUserWorkItem(o => action());
}

public static Action AsynPrincipleWrapper(this Action action)
{
    if (ApplicationContext.ExecutionLocation == ApplicationContext.ExecutionLocations.Client)
    {
        return action; // 客戶端無需處理
    }

    var principelNeed = ApplicationContext.User;
    return () =>
    {
        var oldPrincipel = ApplicationContext.User;
        if (oldPrincipel != principelNeed)
        {
            ApplicationContext.User = principelNeed;
        }
        try
        {
            action();
        }
        finally
        {
            if (oldPrincipel != principelNeed)
            {
                ApplicationContext.User = oldPrincipel;
            }
        }
    };
}

此擴充方法確保手動啟動的執行緒在執行期間使用與啟動者相同的 Principal,執行完畢後自動還原,完美解決非同步執行緒的身分傳遞問題。

小結

本文詳細記錄了如何將 CSLA 框架服務端改造為支援多線程的完整方案:

  1. 透過裝飾模式修改 WCF 服務行為,啟用 ConcurrencyMode.Multiple;
  2. 修正 ApplicationContext.User 的靜態欄位設計,引入 [ThreadStatic] 區分客戶端與服務端儲存策略;
  3. 為手動啟動的非同步執行緒提供 Principal 包裝器,保證執行緒間身分一致性。

這套方案在實際專案中穩定運行,有效提升了服務端併發處理能力。希望能為同樣使用 CSLA 框架的開發者提供參考。

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