在前一篇文章中,我們探討了如何利用非同步線程實現資料預載入,以提升系統效能。這類操作通常在客戶端執行,旨在縮短使用者等待時間。然而,若客戶端發送多個非同步請求,而服務端不支援多線程處理,而是線性逐一處理請求,則客戶端的非同步設計將失去意義。
雖然現代服務端架構理應支援多線程,但我們的系統採用 CSLA 作為分散式框架,其服務端預設僅支援單線程模式。經查閱 CSLA 官方論壇,框架作者指出因 GlobalContext 與 ClientContext 的限制,暫不支援多線程。由於系統已深度依賴 CSLA,短期內難以替換框架,因此我們決定深入原始碼進行修改。
.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
}
接著需在兩處替換類別實例化:
<services>
<service name="OpenExpressApp.Server.WPFHost.MultiThreadsWCFPortal" behaviorConfiguration="returnFaults">
.....
</service>
</services>
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,解決多線程下的身分衝突。
解決上述兩點後,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 框架服務端改造為支援多線程的完整方案:
ConcurrencyMode.Multiple;ApplicationContext.User 的靜態欄位設計,引入 [ThreadStatic] 區分客戶端與服務端儲存策略;這套方案在實際專案中穩定運行,有效提升了服務端併發處理能力。希望能為同樣使用 CSLA 框架的開發者提供參考。