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