Asp.Net在多執行緒環境下的狀態儲存問題

永遠的阿哲發表於2013-06-18

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

      歡迎各路朋友指正.

 

      參考

      HttpContext.Current並非無處不在

      如何實現對上下文(Context)資料的統一管理 [提供原始碼下載]

      CallContext和多執行緒

      HTTPContext across threads

      Do ASP.NET Requests always BeginRequest and EndRequest on the same thread?

      CallContext vs ThreadStatic

      ThreadStatic, CallContext and HttpContext in ASP.Net

      CallContext vs. ThreadStatic vs. HttpContext

      關於執行緒及CallContext

      多執行緒程式設計之計算限制型非同步操作

      CallContext.LogicalGetData Vs. CallContext.GetData

相關文章