在應用開發中,我們經常需要設置一些上下文(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