前篇说到了使用异步线程来实现数据的预加载,以提高系统性能。
这样的操作一般是在客户端执行,用以减少用户的等待时间。客户端发送多次异步请求,到达服务端后,如果服务端不支持多线程处理操作,线性处理各个请求,必然导致客户端的异步请求变得没有意义。
大家肯定会说,谁会把服务端设计成单线程的啊,那不是明显的错误吗?是的!但是我们的系统使用了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 |
} |
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 13 #region IWcfPortal Members 14 15 public WcfResponse Create(CreateRequest request) 16 { 17 return this._innerPortal.Create(request); 18 } 19 20 //... 21 22 #endregion 23 }同时,我们需要把配置文件和类的实例化两处代码都替换: 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 } 这里把原来的字段变为了一个属性!实现它时,如果是在客户端,还是使用一个一般的静态字段。如果是在服务端时,就换成了一个标记了[ThreadStatic]的字段,该标记表示:这个字段会为每一个线程分配独立的值。这样,服务端在请求被处理的开始阶段对_principal赋值时,就存储在了当前线程中,而不会影响其它线程。 手动开启的线程 上面已经解决了两个问题: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 20 var principelNeed = ApplicationContext.User; 21 22 return () => 23 { 24 var oldPrincipel = ApplicationContext.User; 25 if (oldPrincipel != principelNeed) 26 { 27 ApplicationContext.User = principelNeed; 28 } 29 30 try 31 { 32 action(); 33 } 34 finally 35 { 36 if (oldPrincipel != principelNeed) 37 { 38 ApplicationContext.User = oldPrincipel; 39 } 40 } 41 }; 42 } 原来的API改为: 1 public static void SafeInvoke(Action action) 2 { 3 action = action.AsynPrincipleWrapper(); 4 5 ThreadPool.QueueUserWorkItem(o => action()); 6 } 1 这样就实现了:手工打开的线程,使用和打开者线程相同的一个ApplicationContext.User。 1 1 <strong>小结</strong> 1 本文主要介绍了如何把CSLA框架的服务端打造为支持多线程。可能会对使用CSLA框架的朋友会有所帮助。 view sourceprint? 1 下一篇应用一个在GIX4项目中的实例,说明一下在具体项目中如何应用这几篇文章中提到的方法。http://www.cnblogs.com/zgynhqf/archive/2010/07/01/1769228.html