前篇說到了使用異步線程來實現數據的預加載,以提高系統性能。
這樣的操作一般是在客戶端執行,用以減少用戶的等待時間。客戶端發送多次異步請求,到達服務端後,如果服務端不支持多線程處理 操作,線性處理各個請求,必然導致客戶端的異步請求變得沒有意義。
大家肯定會說,誰會把服務端設計成單線程的啊,那不是明顯的錯誤嗎?是的!但是我們的系統使用了CSLA來作為實現分布式的框架, 而它的服務端程序卻只能支持單線程……這個問題我們一直想解決,但是查過CSLA官方論壇,作者說由於GlobalContext和ClientContext 的一些原因,暫時不支持多線程。火大,這還怎麼用啊!無奈目前系統已經極大地依賴了這個框架,一時半會兒要想換一個新的,也不太 現實。所以只好自己動手修改CSLA裡面的代碼了:
修改WCF通信類
要修改為多線程的服務端,首先得從服務端的請求處理處入手。.NET3.5的CSLA框架使用WCF實現數據傳輸。它在服務器端使用這個類來 接收:
1 namespace Csla.Server.Hosts
2 {
3 [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
4 [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
5 public class WcfPortal : IWcfPortal { }
6 }
可以看到,這個類沒有標注ConcurrencyMode = ConcurrencyMode.Multiple和UseSynchronizationContext = false,所以已經被設計 為單線程操作。在這裡,我們使用裝飾模式來構造一個新的類:
01 /// <summary>
02 /// 標記了ConcurrencyMode = ConcurrencyMode.Multiple
03 /// 來表示多線程進行
04 /// </summary>
05 [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall,
06 ConcurrencyMode = ConcurrencyMode.Multiple,
07 UseSynchronizationContext = false)]
08 [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
09 public class MultiThreadsWCFPortal : IWcfPortal
10 {
11 private WcfPortal _innerPortal = new WcfPortal();
12 #region IWcfPortal Members
13 public WcfResponse Create(CreateRequest request)
14 {
15 return this._innerPortal.Create(request);
16 }
17 //...
18 #endregion
19 }
同時,我們需要把配置文件和類的實例化兩處代碼都替換:
app.config:
1 <services>
1 <!--Csla.Server.Hosts.WcfPortal-->
1 <service name="OpenExpressApp.Server.WPFHost.MultiThreadsWCFPortal" behaviorConfiguration="returnFaults">
2 .....
3 </service>
4 </services>
factory method:
1 private static Type GetServerHostType()
2 {
3 return typeof(OpenExpressApp.Server.WPFHost.MultiThreadsWCFPortal);
4 //return typeof(Csla.Server.Hosts.WcfPortal);
5 }
這樣,在服務端接收到請求時,會自動開啟多個線程來響應請求。同時,裝飾模式的使用使得我們不需要對源代碼進行任何更改。
修改ApplicationContext._principal字段
按照上面的操作修改之後,已經在WCF級別上實現了多線程。但是當再次運行應用程序時,會拋出NullRefrenceException異常。代碼出 現在這裡:
1 var currentIdentity = Csla.ApplicationContext.User.Identity as OEAIdentity;
2 currentIdentity.GetDataPermissionExpr(businessObjectId);
調試發現,Csla.ApplicationContext.User是一個UnauthenticatedIdentity的實例。可是我們已經登錄了,這個屬性為什麼還是“未 授權”呢?查看源代碼,發現每次在處理請求的開始階段,CSLA會設置這個屬性為客戶端傳入的用戶標識。那麼我們來看這個屬性在CSLA 中的源代碼:
01 private static IPrincipal _principal;
02 public static IPrincipal User
03 {
04 get
05 {
06 IPrincipal current;
07 if (HttpContext.Current != null)
08 current = HttpContext.Current.User;
09 else if (System.Windows.Application.Current != null)
10 {
11 if (_principal == null)
12 {
13 if (ApplicationContext.AuthenticationType != "Windows")
14 _principal = new Csla.Security.UnauthenticatedPrincipal();
15 else
16 _principal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
17 }
18 current = _principal;
19 }
20 else
21 current = Thread.CurrentPrincipal;
22 return current;
23 }
24 set
25 {
26 if (HttpContext.Current != null)
27 HttpContext.Current.User = value;
28 else if (System.Windows.Application.Current != null)
29 _principal = value;
30 Thread.CurrentPrincipal = value;
31 }
32 }
代碼中顯示,如果服務端使用的是WPF應用程序時,就使用一個靜態字段保存當前的用戶。這就是說服務端的所有線程都只能獲取到最 後一個請求的用戶,當然就不能提供多線程的服務!這裡,其實是作者的一個小BUG:他認為使用WPF的程序應該就是客戶端,所以直接存 儲在靜態變量中。但是我們的服務端也是WPF來實現的,所以就導致了無法為每個線程使用獨立的數據。
這個類同時被客戶端和服務端所使用,所以改動不能影響客戶端的正常使用。為了最少地改動原有代碼,我把字段的代碼修改為:
01 [ThreadStatic]
02 private static IPrincipal __principalThreadSafe;
03 private static IPrincipal __principal;
04 private static IPrincipal _principal
05 {
06 get
07 {
08 return _executionLocation == ExecutionLocations.Client ? __principal : __principalThreadSafe;
09 }
10 set
11 {
12 if (_executionLocation == ExecutionLocations.Client)
13 {
14 __principal = value;
15 }
16 else
17 {
18 __principalThreadSafe = value;
19 }
20 }
21 }
手動開啟的線程
上面已經解決了兩個問題:1、默認沒有打開多線程;2、多個線程對ApplicationContext.User類賦值時,使用靜態字段導致值的沖突 。
這樣就高枕無憂了嗎?答案是不!:)
這樣只是保證了WCF用於處理請求的線程中,ApplicationContext.User屬性的值是正確的。但是我們在處理一個單獨的請求時,又很有 可能手工打開更多的線程來為它服務。這些線程的ApplicationContext.User字段並沒有被CSLA框架賦值,如果這時使用到它時,又會出現 NullRefrenceException……
由於我們進行異步處理時的代碼都是經過一層細微的封裝的,所以這時候好處就體現出來了。我們的處理方案是,在手工申請異步執行 的方法實現中,為傳入的異步操作加一層“包裹器”,例如下面這個API,它是用來給客戶程序調用異步操作的,當時只是封裝了線程池的 簡單調用,為的就是方便將來做擴展(例如我們可以改為Task來實現……)。
1 public static void SafeInvoke(Action action)
2 {
3 ThreadPool.QueueUserWorkItem(o => action());
4 }
我們添加了一個擴展方法如下:
01 /// <summary>
02 /// 這裡生成的wrapper會保證,在執行action前後,新開的線程和主線程都使用同一個Principel。
03 ///
04 /// 解決問題:
05 /// 由於ApplicationContext.User是基於線程的,
06 /// 所以如果在同一次請求中,如果在服務端打開一個新的線程做一定的事情,
07 /// 這個新開的線程可能會和打開者使用不同的Principle而造成代碼異常。
08 /// </summary>
09 /// <param name="action">
10 /// 可能會使用ApplicationContext.User,並需要在服務端另開線程來執行的操作。
11 /// </param>
12 /// <returns></returns>
13 public static Action AsynPrincipleWrapper(this Action action)
14 {
15 if (ApplicationContext.ExecutionLocation == ApplicationContext.ExecutionLocations.Client)
16 {
17 return action;
18 }
19 var principelNeed = ApplicationContext.User;
20 return () =>
21 {
22 var oldPrincipel = ApplicationContext.User;
23 if (oldPrincipel != principelNeed)
24 {
25 ApplicationContext.User = principelNeed;
26 }
27 try
28 {
29 action();
30 }
31 finally
32 {
33 if (oldPrincipel != principelNeed)
34 {
35 ApplicationContext.User = oldPrincipel;
36 }
37 }
38 };
39 }
原來的API改為:
1 public static void SafeInvoke(Action action)
2 {
3 action = action.AsynPrincipleWrapper();
4 ThreadPool.QueueUserWorkItem(o => action());
5 }
這樣就實現了:手工打開的線程,使用和打開者線程相同的一個ApplicationContext.User。
小結
本文主要介紹了如何把CSLA框架的服務端打造為支持多線程。可能會對使用CSLA框架的朋友會有所幫助。
下一篇應用一個在GIX4項目中的實例,說明一下在具體項目中如何應用這幾篇文章中提到的方法。