在应用开发中,我们经常需要设置一些上下文(Context)信息,这些上下文信息一般基于当前的会话(Session),比如当前登录用户的个人信息;或者基于当前方法调用栈,比如在同一个调用中涉及的多个层次之间数据。
在.Net中,常用的有以下三种方法来实现这个特性.
HttpContext.Current.Session或HttpContext.Currnet.Items是大家使用的最多的方式.
[ThreadStatic]方式可以存储单个线程的共享状态.
System.Runtime.Remoting.Messaging.CallContext类则可以存储一个逻辑线程的共享状态,即主线程和其所有子线程都共享这段内存.
在Asp.Net中通常使用第一种方式.但是鱼李写了一篇文章,指出HttpContext.Current并非无处不在,只有是由请求发起的线程,HttpContext.Current才不为空.换句话说,在多线程环境下, 比如是由定时器发起的线程,Currnet属性就为空,这时依赖于它的相关功能便无法完成.比如使用它获取文件路径等.
鱼李给出了两种解决方法,将HttpContext.Current保存在外部变量中,或者通过函数参数传递.
我个人认为这只是折中的解决办法,它没有在解决问题的同时保待简单性,即程序员不需关心HttpContext.Current的保存位置与传递方式,而直接便可使用.
后台又查到了A大的一篇文章.给出了保存下文(Context)信息的通用解决方案,简单来说,在桌面环境使用System.Runtime.Remoting.Messaging.CallContext类,在Web环境下使用常规的HttpContext.Current,但是同时在System.Runtime.Remoting.Messaging.CallContext类中也保存了一个对它的引用.在线程切换时,依照.Net的设计,System.Runtime.Remoting.Messaging.CallContext类中保存的实现了ILogicalThreadAffinative接口的数据都会自动被复制到新的线程中,即完成了上下文传递.
A大给出了一个精妙的解决方案,但却没有解决我所有的疑问,比如48L的园友就提出了"请教一下,callcontxt无论是那种应用都可以使用,为什么还要使用HttpSessionState?".于是我继续探究.终于在一篇老外的博文中找到了答案.
在那篇文章里,老外做了一个试验.有两个页面,均在其构造函数与Page_Load中使用上面三种方式记录当前线程Id,但是在名为slow的页面中人为让处理线程睡一下来模拟耗时操作.首先访问slow页面,在其返回前快速多次刷新fast页面.最终的打印结果让作者surprise了一下.对于slow页面,执行构造函数的线程与Page_Load的线程保持一致,三种方式的记录结果也没有丢失,但是在fast页面中,有可能出现执行构造函数的线程与Page_Load的线程不一致,三种方式的记录结果也丢失了两种:仅剩下HttpContext了.
我的重现代码如下,增加了LogicalSetData方式,后文再表:
public partial class Fast : System.Web.UI.Page { [ThreadStatic] private static string info = string.Empty; public Fast() { info = "fast ctor:" + Thread.CurrentThread.ManagedThreadId; CallContext.SetData("id", Thread.CurrentThread.ManagedThreadId); CallContext.LogicalSetData("id1", Thread.CurrentThread.ManagedThreadId); Items["id2"] = Thread.CurrentThread.ManagedThreadId; } protected void Page_Load(object sender, EventArgs e) { Response.Write("ThreadStatic:" + info); info = string.Empty; Response.Write("<br />"); Response.Write("CallContext.SetData:" + CallContext.GetData("id")); Response.Write("<br />"); Response.Write("CallContext.LogicalSetData:" + CallContext.LogicalGetData("id1")); Response.Write("<br />"); Response.Write("Items:" + Items["id2"]); Response.Write("<br />"); Response.Write("<br />fast page_load:" + Thread.CurrentThread.ManagedThreadId); } } public partial class Slow : System.Web.UI.Page { [ThreadStatic] private static string info = string.Empty; public Slow() { Thread.Sleep(1000); info = "slow ctor:" + Thread.CurrentThread.ManagedThreadId; CallContext.SetData("id", Thread.CurrentThread.ManagedThreadId); CallContext.LogicalSetData("id1", Thread.CurrentThread.ManagedThreadId); Items["id2"] = Thread.CurrentThread.ManagedThreadId; } protected void Page_Load(object sender, EventArgs e) { Thread.Sleep(1000); Response.Write("ThreadStatic:" + info); info = string.Empty; Response.Write("<br />"); Response.Write("CallContext.SetData:" + CallContext.GetData("id")); Response.Write("<br />"); Response.Write("CallContext.LogicalSetData:" + CallContext.LogicalGetData("id1")); Response.Write("<br />"); Response.Write("Items:" + Items["id2"]); Response.Write("<br />"); Response.Write("<br />slow page_load:" + Thread.CurrentThread.ManagedThreadId); } }
执行结果如下:
从博文中获知,这是完全正常的执行结果, Asp.Net开发团队还为其起了个好听的名字:Thread-Agile.这个特性表明,即使你使用常规的Asp.Net开发方式,也不能保证所有的代码一定会在同一线程中执行.从其它的文章中获知,这是与负载有关的.负载越大,越有可能产生多线程.每当程序进入一个新的线程中执行时,Asp.Net会手动(是使用额外代码实现的,不是.Net自带的机制)将HttpContext对象复制到新线程中.一方面这能将多线程完全透明,让程序员使用单线程的编程方式编写多线程程序;另一方面CallContext.SetData与[ThreadStatic]就丢失了.但由于使用LogicalSetData方法存储的数据其内部都会自动封装成实现了ILogicalThreadAffinative接口的对象,所以在线程切换时能正常流转.
所以,在Asp.Net环境下,除非自己建立一套上下文环境解决方案,否则在该用的情况下还是老老实实使用HttpContext吧.
欢迎各路朋友指正.
参考
如何实现对上下文(Context)数据的统一管理 [提供源代码下载]
Do ASP.NET Requests always BeginRequest and EndRequest on the same thread?
ThreadStatic, CallContext and HttpContext in ASP.Net