性能优化总结(五):CSLA服务端如何使用多线程的解决方案
前篇说到了使用异步线程来实现数据的预加载,以提高系统性能。
这样的操作一般是在客户端执行,用以减少用户的等待时间。客户端发送多次异步请求,到达服务端后,如果服务端不支持多线程处理操作,线性处理各个请求,必然导致客户端的异步请求变得没有意义。
大家肯定会说,谁会把服务端设计成单线程的啊,那不是明显的错误吗?是的!但是我们的系统使用了CSLA来作为实现分布式的框架,而它的服务端程序却只能支持单线程……这个问题我们一直想解决,但是查过CSLA官方论坛,作者说由于GlobalContext和ClientContext的一些原因,暂时不支持多线程。火大,这还怎么用啊!无奈目前系统已经极大地依赖了这个框架,一时半会儿要想换一个新的,也不太现实。所以只好自己动手修改CSLA里面的代码了:
修改WCF通信类
要修改为多线程的服务端,首先得从服务端的请求处理处入手。.NET3.5的CSLA框架使用WCF实现数据传输。它在服务器端使用这个类来接收:
namespace Csla.Server.Hosts
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class WcfPortal : IWcfPortal { }
}
可以看到,这个类没有标注ConcurrencyMode = ConcurrencyMode.Multiple和UseSynchronizationContext = false,所以已经被设计为单线程操作。在这里,我们使用装饰模式来构造一个新的类:
/// <summary>
/// 标记了ConcurrencyMode = ConcurrencyMode.Multiple
/// 来表示多线程进行
/// </summary>
[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
}
同时,我们需要把配置文件和类的实例化两处代码都替换:
app.config:
<services>
<!--Csla.Server.Hosts.WcfPortal-->
<service name="OpenExpressApp.Server.WPFHost.MultiThreadsWCFPortal" behaviorConfiguration="returnFaults">
.....
</service>
</services>
factory method:
private static Type GetServerHostType()
{
return typeof(OpenExpressApp.Server.WPFHost.MultiThreadsWCFPortal);
//return typeof(Csla.Server.Hosts.WcfPortal);
}
这样,在服务端接收到请求时,会自动开启多个线程来响应请求。同时,装饰模式的使用使得我们不需要对源代码进行任何更改。
修改ApplicationContext._principal字段
按照上面的操作修改之后,已经在WCF级别上实现了多线程。但是当再次运行应用程序时,会抛出NullRefrenceException异常。代码出现在这里:
var currentIdentity = Csla.ApplicationContext.User.Identity as OEAIdentity;
currentIdentity.GetDataPermissionExpr(businessObjectId);
调试发现,Csla.ApplicationContext.User是一个UnauthenticatedIdentity的实例。可是我们已经登录了,这个属性为什么还是“未授权”呢?查看源代码,发现每次在处理请求的开始阶段,CSLA会设置这个属性为客户端传入的用户标识。那么我们来看这个属性在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应用程序时,就使用一个静态字段保存当前的用户。这就是说服务端的所有线程都只能获取到最后一个请求的用户,当然就不能提供多线程的服务!这里,其实是作者的一个小BUG:他认为使用WPF的程序应该就是客户端,所以直接存储在静态变量中。但是我们的服务端也是WPF来实现的,所以就导致了无法为每个线程使用独立的数据。
这个类同时被客户端和服务端所使用,所以改动不能影响客户端的正常使用。为了最少地改动原有代码,我把字段的代码修改为:
[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;
}
}
}
这里把原来的字段变为了一个属性!实现它时,如果是在客户端,还是使用一个一般的静态字段。如果是在服务端时,就换成了一个标记了[ThreadStatic]的字段,该标记表示:这个字段会为每一个线程分配独立的值。这样,服务端在请求被处理的开始阶段对_principal赋值时,就存储在了当前线程中,而不会影响其它线程。
手动开启的线程
上面已经解决了两个问题:1、默认没有打开多线程;2、多个线程对ApplicationContext.User类赋值时,使用静态字段导致值的冲突。
这样就高枕无忧了吗?答案是不!:)
这样只是保证了WCF用于处理请求的线程中,ApplicationContext.User属性的值是正确的。但是我们在处理一个单独的请求时,又很有可能手工打开更多的线程来为它服务。这些线程的ApplicationContext.User字段并没有被CSLA框架赋值,如果这时使用到它时,又会出现NullRefrenceException……
由于我们进行异步处理时的代码都是经过一层细微的封装的,所以这时候好处就体现出来了。我们的处理方案是,在手工申请异步执行的方法实现中,为传入的异步操作加一层“包裹器”,例如下面这个API,它是用来给客户程序调用异步操作的,当时只是封装了线程池的简单调用,为的就是方便将来做扩展(例如我们可以改为Task来实现……)。
public static void SafeInvoke(Action action)
{
ThreadPool.QueueUserWorkItem(o => action());
}
我们添加了一个扩展方法如下:
/// <summary>
/// 这里生成的wrapper会保证,在执行action前后,新开的线程和主线程都使用同一个Principel。
///
/// 解决问题:
/// 由于ApplicationContext.User是基于线程的,
/// 所以如果在同一次请求中,如果在服务端打开一个新的线程做一定的事情,
/// 这个新开的线程可能会和打开者使用不同的Principle而造成代码异常。
/// </summary>
/// <param name="action">
/// 可能会使用ApplicationContext.User,并需要在服务端另开线程来执行的操作。
/// </param>
/// <returns></returns>
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;
}
}
};
}
原来的API改为:
public static void SafeInvoke(Action action)
{
action = action.AsynPrincipleWrapper();
ThreadPool.QueueUserWorkItem(o => action());
}
这样就实现了:手工打开的线程,使用和打开者线程相同的一个ApplicationContext.User。
小结
本文主要介绍了如何把CSLA框架的服务端打造为支持多线程。可能会对使用CSLA框架的朋友会有所帮助。
下一篇应用一个在GIX4项目中的实例,说明一下在具体项目中如何应用这几篇文章中提到的方法。
- 关于C#获取动态的时间差函数
- SQL Server 存储过程
- ubuntu中配置hadoop
- jQuery选择器大全(48个代码片段+21幅图演示)
- C# 如何在Excel 动态生成PivotTable
- RabbitMQ入门HelloWorld(C#)(翻译)
- Centos环境下搭建Asp.NET Core环境和安装Jexus
- Linux系统Java环境安装配置
- ASP.NET Core 依赖注入
- 使用Hive SQL插入动态分区的Parquet表OOM异常分析
- 基于STS和JWT的微服务身份认证
- 小程序前端开发基础框架,可直接用于开发
- “盲”逆向:iOS 应用 Blind 寻踪
- Redis应用及安装
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- PHP实现微信退款的方法示例
- 基于Python和C++实现删除链表的节点
- python让函数不返回结果的方法
- PHP微商城开源代码实例
- PHP小程序支付功能完整版【基于thinkPHP】
- CodeIgniter框架实现的整合Smarty引擎DEMO示例
- PHP微信支付功能示例
- PHP中ltrim()函数的用法与实例讲解
- Laravel 中创建 Zip 压缩文件并提供下载的实现方法
- pytorch随机采样操作SubsetRandomSampler()
- Pytorch上下采样函数–interpolate用法
- scrapy框架携带cookie访问淘宝购物车功能的实现代码
- 浅析Python __name__ 是什么
- PHP判断访客是否手机端(移动端浏览器)访问的方法总结【4种方法】
- PHP后期静态绑定实例浅析