2009年2月22日

無法預測、無法控制的 ASP.NET multi-thread 機制

所謂的 thread 是甚麼相信大家都知道,多少也在學生時代寫過一兩個多執行緒的作業。
在 .NET 的世界裡面,無論是Web Apps or Windows Apps,在 runtime 時期都是在 CLR 上運作,而 CLR 所看到的都是在 AppDomain 中執行的 thread,
Web/Windows Apps 的分別只是在於 CLR 載入的 aassembly 不同罷了。

從上上週末開始,我們 Team 就開始設法利用 TLS 來解決舊元件造成的 MSDTC 問題,
在想像中這個 solution 可以同時滿足 Web Apps & Windows Apps ,
沒想到就此一頭栽進了深不見底的 ASP.NET multi-thread 地獄。
(到現在還是處於一頭霧水的狀態 Orz)


經過5個系統的實測(4個是各部門的系統,另一個是自己寫的超小型 demo 網站),
我們發現「ASP.NET 在某些不明條件下,會以 multi-thread 的方式來處理 Request」,
也就是「custom httpModule 與 page 的 code-behind 是以不同的 Thread 在運作」,
這與我們想像中的「ASP.NET 對一個 Request 從頭到尾都是以單一 Thread 來處理」的假設完全不同,而這種現象使得 TLS Solution 呈現完全報廢的狀態,為甚麼呢?

What’s ThreadLocalStorage 這篇文章中所附的範例程式中,我是在一個自己撰寫的 custom httpModule 中去處理 TLS 中的資料,若 ASP.NET 的行為一如預期的是以單一 Thread 來處理 Request,那麼在page 的 code-behind 程式 中便可順利取得在 custom httpModule 中存入 TLS 的資料(connection & transaction 物件),也就可以執行 local transactions 而不需勞師動眾的啟動 distributed transaction (MSDTC) 了。

但是當 ASP.NET 以新的 Thread (#2) 來處理 page 的 code-behind 程式時,Thread #2 是無法取得 custom httpModule (Thread #1) 的 TLS 中的 connection & trnasaction 物件的(不然怎麼叫 ThreadLocalStorage 呢?),因此在 code-behind 的程式中要 access DB 時便會出現 NullReferenceException

奇怪的是,在拿來實測的系統中完全沒有撰寫 multi-thread 的程式(e.g., Thread.Start()),因此目前推測 multi-thread 現象是 ASP.NET 內部所做的 (Performance) Optimization 所產生的結果,是我們的程式無法(起碼很難)控制的。

在5個實測過的系統中,出現以下的 multi-thread 現象:
(System E 是自己寫的超小型 demo 網站)
  • 有插入 custom httpModule:
    • System A: httpModule 屬於 Thread #1,page 屬於 Thread #2
    • System B: httpModule 屬於 Thread #1 & #2,page 屬於 Thread #3 ~ #n
    • System C: httpModule 與 page 屬於 Thread #1 (使用單一 Thread!)
    • System D: httpModule 屬於 Thread #1,page 屬於 Thread #2
    • System E: httpModule 與 page 屬於 Thread #1 (使用單一 Thread!)
  • 插入 custom httpModule (也就是一般寫網站的狀況)
    • System A: page 屬於 Thread #1 ~ #n
    • System B: page 屬於 Thread #1 (使用單一 Thread!)
    • System C: 沒有測到
    • System D: page 屬於 Thread #1 (使用單一 Thread!)
    • System E: httpModule 與 page 屬於 Thread #1 (使用單一 Thread!)
根據上述現象,目前推測與 ASP.NET multi-thread 機制無關的因素包括:
  • .NET Framework 版本 (1.1/2.0/3.5)
  • IIS 版本 (6.0/7.0)
  • 是否(混合)使用 MasterPage、UserControl、CustomControl
  • Page 是否繼承自己包的 BasePage
  • 頁面是否有用 frame 切割
而推測可能有關的因素包括:
  • 某種程式寫法會造成 ASP.NET 內部自動改以 multi-thread 來執行
    • 在 page 中以 js 另開視窗(做一些事情)之後再關閉的程式寫法
    • 在自己寫的超小型 demo 網站中,MasterPage、BasePage、UserControl 中的 Init / OnLoad 事件中的程式碼都是空的,不會啟動 multi-thread
  • 註冊 custom httpModule (不註冊的話,使用單一 Thread 的機率高很多)
  • 第一次 Request 與 (Ctrl+) F5 Refresh 會造成不同的效果 (不同數量的 Thread)
雖然目前還是不清楚 ASP.NET 啟動 multi-thread 機制的條件為何,但已經很明顯得到一個「TLS 此路不通」的結論,目前也沒找到比較深入的官方 reference(不過倒是曾經聽過在 ASP.NET 中最好不要自己寫 multi-thread 的程式的說法),因此這個有趣的謎只能等有空再研究囉~~

那麼要如何觀測系統是否有啟動 multi-thread 呢?以下是觀測的步驟,有興趣的人可以拿自己的系統試試看:
  1. 進入 Visual Studio 的 Debug Mode。或者先以 IIS/ASP.NET Development Server (lightweight 的那個)瀏覽到目標頁面後,將 Visual Studio 附加至該網頁的處理序上進行 debug。
  2. 在該 aspx 以及 BasePage、MasterPage、UserControl 等檔案的 code-behind 程式中的 Init / OnLoad 事件上設定中斷點。
  3. 觀察執行過程中「System.Threading.Thread.CurrentThread.ManagedThreadId」的值。

5 則留言:

匿名 提到...

TLS (ThreadLocalStorage) 行不通的話,改用 HttpContext.Current.Items 看看,也許是個除了 TLS 之外另一個選擇...

Unknown 提到...

To chicken:

感謝你的建議!經過這個禮拜的測試,還有 MSDN 上的說明,看來 HttpContext.Current.Items 就是符合目前需求的 per-request data store ... 下週要進行壓力測試,看看擺脫 MSDTC 之後可以增進多少效能 :D

匿名 提到...

有用的話真是太好了 :D

我也碰到類似的問題,有自行開發的 ORM, 不過我們的問題麻煩了點,除了 ASP.NET 之外,也包裝成 COM 給 ASP 用... 在 ASP 端還沒找到妥善的 solution ...

Unknown 提到...

印象中 ASP 都是以 Single Threaded Apartment (STA) 的模式運作的?(http://msdn.microsoft.com/en-us/library/aa479019.aspx)最近遇到一個 ASP.NET 網站在某台 VM 上出現「An error was encountered while calling OnStartPage in ASP compatibility mode.」的錯誤訊息,因為 .aspx 的 page 屬性中設定了「aspcompat=true」(為了讓 COM 元件正常運作),這錯誤好像很罕見...>_< Anyway,或許把 .NET Assembly 包裝成 Web Services 發布給 ASP 呼叫,可以減少一些問題。(把 .NET Assembly 包裝成 COM-visible,或者把 COM 作成 Intreop 元件感覺都是逼不得已的選擇...)

匿名 提到...

是的,逼不得已的選擇 :~

為了不讓 ASP / ASP.NET 有兩套做一樣事情的元件,最後就選擇 COM interop ...

Google Spreadsheet 裡用規則運算式

最近因為工作關係,遇到要用 Google Form 及 Google Sheet 所以研究了 Google Sheet 裡的一些 function 怎麼用 首先,分享一下如何在 Google Sheet 裡用規則運算 :D