許多開發者誤以為 IHttpModule.Init 等同於 Global.asax 的 Application_Start,並在其中執行應用程式層級的一次性初始化 (如建立靜態快取、註冊全域事件)。實際上,Init 在高併發環境下會被多次呼叫,導致重複初始化、資源洩漏或競態條件。本文深入剖析原因並提供正確解決方案。
ASP.NET 執行緒池模型:每個 HttpApplication 執行個體同一時間只能處理一個請求。當併發請求超過現有執行個體數時,ASP.NET 會建立新的 HttpApplication 執行個體,每個執行個體在建構時都會實例化所有已註冊的 IHttpModule 並呼叫其 Init 方法。
Application_Start:僅在**第一個** HttpApplication 建立後觸發一次,後續執行個體不再觸發。IHttpModule.Init:**每個** HttpApplication 執行個體建立時都會觸發。因此,若在 Init 中執行「僅需執行一次」的邏輯 (如建立單例、載入設定檔、啟動背景工作),在併發環境下就會重複執行。
可在 Init 中記錄 Thread.CurrentThread.ManagedThreadId 與 HttpContext.Current.ApplicationInstance.GetHashCode(),壓力測試時即可觀察到多個不同的 Application 實例觸發 Init。
在 Global.asax.cs 的 Application_Start 執行一次性初始化,這是設計初衷。
public class MyModule : IHttpModule
{
private static bool _initialized;
private static readonly object _lock = new object();
public void Init(HttpApplication context)
{
// 註冊每個請求都需綁定的事件 (如 BeginRequest)
context.BeginRequest += OnBeginRequest;
// 一次性初始化
if (!_initialized)
{
lock (_lock)
{
if (!_initialized)
{
PerformOneTimeInitialization();
_initialized = true;
}
}
}
}
private void PerformOneTimeInitialization()
{
// 載入設定、建立快取、啟動排程器等
}
private void OnBeginRequest(object sender, EventArgs e)
{
// 每個請求執行的邏輯 (URL Rewrite 等)
}
public void Dispose() { }
}
| 操作 | 放在 Init | 放在靜態旗標區塊 |
|---|---|---|
| 註冊事件 (BeginRequest, AuthenticateRequest 等) | ✅ 每個 Module 實例都必須註冊 | ❌ 只註冊一次會導致部分請求未處理 |
| 載入靜態設定、建立單例、啟動背景工作 | ❌ 重複執行風險 | ✅ 確保僅執行一次 |
.NET 4.0+ 可在組件層級標記 [assembly: PreApplicationStartMethod(typeof(Startup), "Initialize")],在 Application_Start **之前** 執行初始化,適合註冊動態模組、建立路由等。
IHttpModule.Init ≠ Application_Start,前者為每個 HttpApplication 實例執行,後者為應用程式生命週期一次。Init 執行;一次性初始化**必須**用靜態旗標保護或移至 Application_Start。Init 進行一次性初始化,在開發環境 (單一請求) 難以發現,上線後併發時才暴露詭異 Bug。