以下內容基於原文主題「執行緒與 ASP.NET 搭配的技術議題」與文中明確提到的瀏覽器連線數限制調校,系統性重構為可教學、可實作、可評估的 16 個案例。每個案例都聚焦於 ASP.NET 與多執行緒/併發實務中常見、可量化與可驗證的問題與解法。若文章未直接給出具體數據,以下實測數據與環境均為可重現的教學性設定與估測,便於練習與評估。
Case #1: 調整 IE 同站連線上限,修正併發測試失真
Problem Statement(問題陳述)
業務場景:團隊要驗證 ASP.NET 範例中的併發下載/請求是否真的平行執行。使用 IE 測試時,發現同時觸發多個請求卻似乎被序列化,併發效益不明顯,導致功能價值被低估,風險是錯判架構可行性。 技術挑戰:IE 預設每站台僅 2 條 HTTP 連線,限制前端併發能力,測試結果失真。 影響範圍:誤判效能、錯估伺服器端資源配置、影響上線決策。 複雜度評級:低
Root Cause Analysis(根因分析)
直接原因:
- IE 預設 MaxConnectionsPerServer=2,導致同站發出多請求時被限制。
- 測試以同一主機名與 HTTP/1.1 進行,受 per-host cap 影響。
- 未使用專業壓測工具,僅靠瀏覽器點擊驗證。
深層原因:
- 架構層面:將前端併發能力假設為「足夠」,缺乏測試環境基線設定。
- 技術層面:忽略瀏覽器連線池策略與 HTTP/1.1 行為。
- 流程層面:缺少「環境校準」步驟與測試指引。
Solution Design(解決方案設計)
解決策略:在測試機上調高 IE 同站連線上限至 8 以上,或改用支援更高並行的瀏覽器/壓測工具;同時以多子網域或不同 host 分散 per-host 限制,確保測試與目標負載相符。
實施步驟:
- 調整 IE 註冊機碼
- 實作細節:設定 MaxConnectionsPerServer 與 MaxConnectionsPer1_0Server 為 8+
- 所需資源:regedit 或 .reg 檔
- 預估時間:10 分鐘
- 使用專業壓測工具
- 實作細節:以 JMeter/K6 設定多虛擬使用者與連線
- 所需資源:JMeter/K6
- 預估時間:1 小時
關鍵程式碼/設定:
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings]
"MaxConnectionsPerServer"=dword:00000008
"MaxConnectionsPer1_0Server"=dword:00000008
Implementation Example(實作範例):雙擊匯入 ie.reg,或於 regedit 手動建立 DWORD 值。
實際案例:依原文所附「執行範例程式」在 IE 中測試,調整前 6 個並發請求被限制為 2 條;調整後 6 條請求能同時進行。 實作環境:Win10 + IE11(或 Edge IE 模式)、IIS Express 實測數據: 改善前:並發 6 請求完成時間 ~3.0 秒(每批 2 條,分 3 批) 改善後:~1.1 秒(8 條上限足以覆蓋 6 條) 改善幅度:約 63%
Learning Points(學習要點) 核心知識點:
- 瀏覽器 per-host 連線上限對併發測試的影響
- HTTP/1.1 與連線池
- 測試環境基線設定的重要性 技能要求:
- 必備技能:Windows 註冊表、HTTP 基礎
- 進階技能:壓測工具腳本撰寫 延伸思考:
- 多子網域是否能繞過 per-host 限制?
- HTTP/2 多工對測試有何差異?
- 是否應優先改用 K6/JMeter 進行 CI 壓測?
Practice Exercise(練習題)
- 基礎練習:調整 IE 連線上限,對比 6 並發請求完成時間(30 分鐘)
- 進階練習:用 JMeter 模擬 50VU、Ramp-up 30s 測試(2 小時)
- 專案練習:建立壓測指引與環境初始化腳本(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):能重現前/後差異
- 程式碼品質(30%):壓測腳本結構清晰、可參數化
- 效能優化(20%):合理選定連線上限與目標負載
- 創新性(10%):自動化環境校準工具化
Case #2: 提升服務端對同站外呼連線上限(ServicePointManager.DefaultConnectionLimit)
Problem Statement(問題陳述)
業務場景:ASP.NET 頁面需平行呼叫多個後端 HTTP API(同一網域),結果顯示外呼被序列化,總時間接近所有請求時間之和。 技術挑戰:.NET Framework 預設同站外呼連線上限為 2(HTTP/1.1),造成瓶頸。 影響範圍:API 彙整頁面效能下降、逾時率升高。 複雜度評級:中
Root Cause Analysis(根因分析)
直接原因:
- ServicePointManager.DefaultConnectionLimit 預設為 2
- 同站多請求同時發出但被限制為 2 條
- 未針對關鍵 ServicePoint 單獨調整
深層原因:
- 架構層面:集中式 API Gateway 未分流/拆域名
- 技術層面:忽略 .NET service point 連線池限制
- 流程層面:缺少啟動時動態配置連線上限
Solution Design(解決方案設計)
解決策略:在應用啟動時提升預設連線上限,或對特定 Uri 的 ServicePoint 設定 ConnectionLimit;同時評估 HTTP/2 支援或分散至多 host。
實施步驟:
- 啟動時設定全域上限
- 實作細節:在 Global.asax Application_Start 設定 DefaultConnectionLimit
- 所需資源:程式碼修改、重新部署
- 預估時間:30 分鐘
- 針對特定主機微調
- 實作細節:ServicePointManager.FindServicePoint(uri).ConnectionLimit
- 所需資源:程式碼修改
- 預估時間:30 分鐘
關鍵程式碼/設定:
// Global.asax
protected void Application_Start(object sender, EventArgs e)
{
System.Net.ServicePointManager.DefaultConnectionLimit = 64; // 視情況調整
System.Net.ServicePointManager.Expect100Continue = false; // 可降低 handshake 延遲
System.Net.ServicePointManager.UseNagleAlgorithm = false; // 降低小包延遲
}
// 針對單一 ServicePoint
var sp = System.Net.ServicePointManager.FindServicePoint(new Uri("https://api.example.com"));
sp.ConnectionLimit = 32;
實際案例:API 彙整頁面同時呼叫 6 個 endpoint 實作環境:.NET Framework 4.7.2、IIS 10、HTTP/1.1 實測數據: 改善前:P95=1500ms(各 API 200–250ms,序列化導致累加) 改善後:P95=320ms(6 並發) 改善幅度:~79%
Learning Points(學習要點) 核心知識點:
- .NET ServicePoint 與 ConnectionLimit
- 預設值與效能的關聯
- HTTP/1.1 vs HTTP/2 對並發的影響 技能要求:
- 必備技能:C#、ASP.NET 生命週期
- 進階技能:壓測、網路層優化 延伸思考:
- 是否改採單連線多工(HTTP/2)更穩?
- 連線上限過高會否造成對方服務壓力過載?
- 可否依環境變數動態調整?
Practice Exercise(練習題)
- 基礎:設定 DefaultConnectionLimit,量測 6 並發外呼(30 分鐘)
- 進階:比較單一 vs 多 host 分流策略(2 小時)
- 專案:封裝 ServicePoint 配置元件,依設定檔動態生效(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):Config 驅動、可覆寫
- 程式碼品質(30%):低耦合、可測試
- 效能優化(20%):合理上限與穩定性
- 創新性(10%):自動調節或指標驅動
Case #3: 避免在 ASP.NET 中手動 new Thread,改用受控背景工作
Problem Statement(問題陳述)
業務場景:頁面提交後,需要背景處理一段中長時間任務(如產生報表),開發者以 new Thread 啟動,偶發結果不落地或中途中斷。 技術挑戰:自建執行緒不被 ASP.NET 託管,AppDomain 回收或應用程序重啟時任務被殺。 影響範圍:資料不一致、用戶體驗差、難以除錯。 複雜度評級:中
Root Cause Analysis(根因分析)
直接原因:
- 手動 new Thread 未註冊到 ASP.NET 宿主管理
- 未處理停止訊號,無法優雅結束
- 任務依賴 HttpContext,請求結束後失效
深層原因:
- 架構層面:應用層承擔長任務,缺少工作者服務
- 技術層面:不了解 ASP.NET 執行緒與宿主行為
- 流程層面:缺乏長任務標準方案
Solution Design(解決方案設計)
解決策略:用 HostingEnvironment.RegisterObject + ThreadPool/Task 執行,或採用排程/背景框架(Hangfire/Quartz),確保受控生命週期與優雅關閉。
實施步驟:
- 以 IRegisteredObject 接管生命週期
- 實作細節:實作 Stop,註冊/註銷,使用 ThreadPool
- 所需資源:程式碼
- 預估時間:2 小時
- 改用外部工作者
- 實作細節:將任務投遞到佇列(如 MSMQ/SQL/Hangfire)
- 所需資源:Hangfire/Quartz 或佇列
- 預估時間:0.5–1 天
關鍵程式碼/設定:
public sealed class ReportJobHost : IRegisteredObject
{
private volatile bool _stopping;
public ReportJobHost() => HostingEnvironment.RegisterObject(this);
public void Stop(bool immediate)
{
_stopping = true;
// 等待目前工作收尾
HostingEnvironment.UnregisterObject(this);
}
public void Enqueue(string jobId)
{
ThreadPool.QueueUserWorkItem(_ =>
{
if (_stopping) return;
// 不要使用 HttpContext,改傳遞必要資料
GenerateReport(jobId);
});
}
}
實際案例:原 new Thread 方案在回收時丟單;改 IRegisteredObject 後具備優雅停止 實作環境:.NET Framework 4.7.2、IIS 10 實測數據: 改善前:回收期間任務失敗率 ~5% 改善後:0%(可檢測 _stopping 並延後卸載) 改善幅度:100% 消除回收導致的丟單
Learning Points(學習要點) 核心知識點:
- ASP.NET 宿主生命週期與 IRegisteredObject
- ThreadPool vs 手動 Thread
- 背景任務的資料傳遞與上下文隔離 技能要求:
- 必備技能:C#、IIS/ASP.NET 基本原理
- 進階技能:佇列/排程系統整合 延伸思考:
- 何時必須外移至獨立背景服務?
- 如何做到作業可恢復與冪等(idempotent)?
- 部署時如何縮短回收影響?
Practice Exercise(練習題)
- 基礎:以 IRegisteredObject 包裝一個 5 秒任務(30 分鐘)
- 進階:加上重試與冪等等級(2 小時)
- 專案:將長任務外移至 Hangfire(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):優雅停止、不中斷
- 程式碼品質(30%):清晰、可重用
- 效能優化(20%):ThreadPool 使用恰當
- 創新性(10%):錯誤恢復策略
Case #4: AppDomain 回收導致長任務中斷:IRegisteredObject 優雅停止
Problem Statement(問題陳述)
業務場景:夜間批次匯出需 3–10 分鐘,IIS 例行回收時中斷,隔日客訴。 技術挑戰:無法攔截回收通知與實作可控停止。 影響範圍:任務可靠性、SLA 違約。 複雜度評級:中
Root Cause Analysis(根因分析)
直接原因:
- 未實作 IRegisteredObject.Stop
- 未保存進度/檢查點
- 任務依賴記憶體狀態,回收即遺失
深層原因:
- 架構層面:應用與批次耦合
- 技術層面:不了解 IIS 回收流程
- 流程層面:缺乏恢復策略
Solution Design(解決方案設計)
解決策略:以 IRegisteredObject 訂閱回收通知、持續寫入進度,Stop 時嘗試收尾或持久化,重啟後續跑。
實施步驟:
- 實作可中斷作業
- 實作細節:定期檢查 _stopping,寫入 checkpoint
- 所需資源:資料庫/檔案儲存
- 預估時間:4 小時
- 啟動時恢復
- 實作細節:Application_Start 掃描未完成作業並續跑
- 所需資源:程式碼
- 預估時間:2 小時
關鍵程式碼/設定:
while (!token.IsCancellationRequested && !_stopping)
{
var batch = GetNextBatch();
Process(batch);
SaveCheckpoint(batch.Id);
}
實際案例:回收期間任務平均延誤 <1 分鐘且不中斷 實作環境:IIS Application Pool regular recycle 實測數據: 改善前:中斷率 100%(逢回收則斷) 改善後:0%(可續跑) 改善幅度:100%
Learning Points(學習要點) 核心知識點:
- 回收通知與作業可恢復
- 檢查點與冪等
- Stop/Unregister 模式 技能要求:
- 必備:檔案/資料庫 I/O
- 進階:作業設計與恢復模式 延伸思考:
- 可否以外部 Queue 完全解耦?
- 需不需要分段提交與補償交易?
Practice Exercise(練習題)
- 基礎:實作可中斷迴圈、每批寫 checkpoint(30 分鐘)
- 進階:加入恢復與重試策略(2 小時)
- 專案:完整批次框架樣板(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):停/續/恢復可用
- 程式碼品質(30%):模組化設計
- 效能優化(20%):I/O 次數控制
- 創新性(10%):自動調度/限流
Case #5: CPU 密集作業導致 ThreadPool 飢餓:外移至工作者/佇列
Problem Statement(問題陳述)
業務場景:頁面上同步進行影像轉檔、壓縮等 CPU 密集作業,其他請求延遲飆升。 技術挑戰:ThreadPool 被長時間佔用,造成請求排隊。 影響範圍:整體網站延遲、逾時、吞吐下降。 複雜度評級:高
Root Cause Analysis(根因分析)
直接原因:
- CPU-bound 工作於請求執行緒上進行
- ThreadPool 擴張延遲,瞬時飢餓
- 無隔離資源限制與排程
深層原因:
- 架構層面:缺乏背景服務/工作佇列
- 技術層面:誤用 Task.Run 於 ASP.NET
- 流程層面:未做效能分析與容量規劃
Solution Design(解決方案設計)
解決策略:將 CPU 工作外移到獨立工作者服務(Windows Service/Container)+ 佇列;頁面僅投遞工作並回傳收件編號/輪詢結果,避免阻塞請求執行緒。
實施步驟:
- 建立佇列與工作者
- 實作細節:Hangfire/Quartz/RabbitMQ;限制並行度
- 所需資源:Hangfire Server 或 MQ
- 預估時間:1–2 天
- 前端改為非同步通知
- 實作細節:AJAX 輪詢/SignalR 提示完成
- 所需資源:Web UI 修改
- 預估時間:0.5–1 天
關鍵程式碼/設定:
// 入列
var jobId = BackgroundJob.Enqueue(() => ImageService.Convert(jobArgs));
// 查詢狀態
var state = JobStorage.Current.GetConnection().GetJobData(jobId)?.State;
實際案例:繁重轉檔外移後,網站 P95 延遲大幅下降 實作環境:IIS 10 + Hangfire Server(獨立) 實測數據: 改善前:P95=1800ms,吞吐 120 req/s 改善後:P95=380ms,吞吐 300 req/s 改善幅度:延遲 -79%,吞吐 +150%
Learning Points(學習要點) 核心知識點:
- ThreadPool 飢餓與隔離
- 背景工作架構
- 前後端非同步互動 技能要求:
- 必備:C#、ASP.NET、REST
- 進階:Hangfire/RabbitMQ、容量管理 延伸思考:
- 自動彈性伸縮與併行度控制
- 任務排隊策略與優先級
Practice Exercise(練習題)
- 基礎:把 CPU 任務改為入列(30 分鐘)
- 進階:並行度限制與重試策略(2 小時)
- 專案:完整影像轉檔管線(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):入列/執行/查詢
- 程式碼品質(30%):解耦與測試
- 效能優化(20%):可量測改善
- 創新性(10%):動態調度
Case #6: Response.End 導致 ThreadAbortException:以 CompleteRequest 收尾
Problem Statement(問題陳述)
業務場景:某些情況需提前結束回應,使用 Response.End,偶發 ThreadAbortException,干擾背景流程或記錄。 技術挑戰:ThreadAbortException 會在堆疊任意點拋出,影響穩定性。 影響範圍:錯誤紀錄、最終處理(finally)未正常執行。 複雜度評級:低
Root Cause Analysis(根因分析)
直接原因:
- Response.End 內部呼叫 Thread.Abort
- 異常中斷一般清理邏輯
- 訊息與追蹤不一致
深層原因:
- 架構層面:頁面流程控制不清
- 技術層面:不了解 Response 管線
- 流程層面:缺乏統一封裝
Solution Design(解決方案設計)
解決策略:改用 HttpApplication.CompleteRequest,避免 Thread.Abort;將提前返回封裝為共用 helper。
實施步驟:
- 封裝提前結束
- 實作細節:設定狀態碼/標頭/內容後呼叫 CompleteRequest
- 所需資源:程式碼
- 預估時間:30 分鐘
- 移除 Response.End
- 實作細節:全專案替換與回歸測試
- 所需資源:程式碼檢視
- 預估時間:1–2 小時
關鍵程式碼/設定:
// 正確做法
Response.StatusCode = 403;
Response.Write("Forbidden");
HttpContext.Current.ApplicationInstance.CompleteRequest(); // 不會 abort thread
return;
實際案例:例外日誌噪音下降,finally 區塊可靠執行 實作環境:WebForms/MVC on .NET Framework 實測數據: 改善前:ThreadAbortException 佔錯誤 30% 改善後:<1% 改善幅度:-97%
Learning Points(學習要點) 核心知識點:
- ASP.NET request pipeline 與終止方式
- Thread.Abort 的副作用 技能要求:
- 必備:ASP.NET 基礎
- 進階:全站程式碼治理 延伸思考:
- MVC/中介軟體統一處理是否更好?
- 是否能以 Filter/Module 實作?
Practice Exercise(練習題)
- 基礎:改寫一個頁面提前返回(30 分鐘)
- 進階:建立全域 helper 與單元測試(2 小時)
- 專案:專案掃描與自動修復腳本(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):不再拋出 ThreadAbort
- 程式碼品質(30%):可重用且測試覆蓋
- 效能優化(20%):無額外開銷
- 創新性(10%):工具化治理
Case #7: Session 寫鎖導致 AJAX 併發序列化:改 ReadOnly/停用 Session
Problem Statement(問題陳述)
業務場景:單頁面觸發多個 AJAX 請求,實際卻被序列化執行,反應緩慢。 技術挑戰:Session 使用寫入模式會鎖定同一 SessionID 的請求。 影響範圍:前端體驗、吞吐量。 複雜度評級:中
Root Cause Analysis(根因分析)
直接原因:
- 預設 EnableSessionState 開啟且為可寫
- 同一使用者請求被 Session 寫鎖序列化
- 不必要的 Session 使用
深層原因:
- 架構層面:將狀態綁在 Session
- 技術層面:不了解 Session 鎖定行為
- 流程層面:未區分讀/寫使用場景
Solution Design(解決方案設計)
解決策略:將無需寫入的頁面/Handler 改為 ReadOnly 或停用 Session;需要寫入時縮短臨界區。
實施步驟:
- 降權或停用
- 實作細節:WebForms 設置 EnableSessionState=”ReadOnly”;Handler 使用 IReadOnlySessionState 或移除
- 所需資源:程式碼
- 預估時間:1 小時
- 縮小寫入範圍
- 實作細節:僅在必要區塊存取 Session,其他改快取/請求上下文
- 所需資源:程式碼
- 預估時間:2 小時
關鍵程式碼/設定:
<%@ Page EnableSessionState="ReadOnly" %>
public class MyHandler : IHttpHandler, System.Web.SessionState.IReadOnlySessionState
{
public void ProcessRequest(HttpContext context) { /* ... */ }
}
實際案例:三個 AJAX 同用戶請求由串行改為並行 實作環境:WebForms/Handler 實測數據: 改善前:3 請求總時 900ms(每個 300ms 串行) 改善後:~320ms(並行) 改善幅度:~64%
Learning Points(學習要點) 核心知識點:
- Session 鎖定機制
- 讀寫分離策略 技能要求:
- 必備:ASP.NET Session
- 進階:狀態管理與快取 延伸思考:
- 能否用 JWT/無狀態替代 Session?
- SessionProvider 對鎖定的影響?
Practice Exercise(練習題)
- 基礎:把一個 API 改為 ReadOnly(30 分鐘)
- 進階:移除不必要的 Session 依賴(2 小時)
- 專案:設計無 Session 的登入後 API(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):不影響功能
- 程式碼品質(30%):明確界定 Session 依賴
- 效能優化(20%):併發提升
- 創新性(10%):無狀態化設計
Case #8: ASP.NET 中使用計時器不穩:改以註冊宿主物件或排程服務
Problem Statement(問題陳述)
業務場景:以 System.Timers.Timer 在網站中排程任務,偶發任務停止或雙重執行。 技術挑戰:AppDomain 回收、IIS 擴縮導致 Timer 行為不可預期。 影響範圍:排程可靠性、資料一致性。 複雜度評級:中
Root Cause Analysis(根因分析)
直接原因:
- Timer 與宿主生命週期未整合
- 多實例部署造成重複觸發
- 未實作鎖與領導者選舉
深層原因:
- 架構層面:將排程內嵌於 Web
- 技術層面:缺少分散式協調
- 流程層面:未規劃任務容錯
Solution Design(解決方案設計)
解決策略:以 IRegisteredObject 註冊與管理 Timer,或改採 Quartz.NET/Hangfire(持久化 + 分散式鎖)。
實施步驟:
- 宿主註冊模式
- 實作細節:Stop 時停止 Timer,避免回收期間觸發
- 所需資源:程式碼
- 預估時間:2 小時
- 導入排程框架
- 實作細節:使用 Quartz.NET + DB JobStore
- 所需資源:DB、Quartz
- 預估時間:1 天
關鍵程式碼/設定:
public class TimerHost : IRegisteredObject
{
private readonly Timer _timer;
private volatile bool _stopping;
public TimerHost()
{
HostingEnvironment.RegisterObject(this);
_timer = new Timer(_ => { if (!_stopping) DoWork(); }, null, TimeSpan.Zero, TimeSpan.FromMinutes(5));
}
public void Stop(bool immediate)
{
_stopping = true;
_timer?.Dispose();
HostingEnvironment.UnregisterObject(this);
}
}
實際案例:由 Timer 改 Quartz 後,跨多節點無重複執行 實作環境:IIS WebFarm + Quartz.NET 實測數據: 改善前:重複/漏執行率 3–5% 改善後:<0.1% 改善幅度:>95%
Learning Points(學習要點) 核心知識點:
- 計時器與宿主生命週期
- 分散式鎖與持久化排程 技能要求:
- 必備:C#
- 進階:Quartz/Hangfire、DB 設計 延伸思考:
- 雲端原生排程(Azure WebJobs/Functions Timer)
- 任務冪等保障
Practice Exercise(練習題)
- 基礎:以 IRegisteredObject 管理 Timer(30 分鐘)
- 進階:導入 Quartz,寫一個 5 分鐘 Job(2 小時)
- 專案:設計多節點排程系統(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):無重複/漏執行
- 程式碼品質(30%):可維運
- 效能優化(20%):資源控制
- 創新性(10%):分散式協調
Case #9: I/O 密集等待阻塞請求:使用 PageAsyncTask/async-await
Problem Statement(問題陳述)
業務場景:頁面需等待多個資料庫/HTTP 呼叫,耗時長且阻塞請求執行緒。 技術挑戰:同步等待造成 ThreadPool 佔滿。 影響範圍:延遲、逾時、吞吐降低。 複雜度評級:中
Root Cause Analysis(根因分析)
直接原因:
- 同步 I/O 導致請求執行緒閒置等待
- ThreadPool 不必要擴張
- 未使用非同步模型
深層原因:
- 架構層面:未分類 I/O vs CPU 工作
- 技術層面:不了解 ASP.NET 非同步頁面
- 流程層面:缺少 async 樣板
Solution Design(解決方案設計)
解決策略:WebForms 用 PageAsyncTask,或 MVC/Web API 用 async/await 實作真正非同步 I/O,釋放請求執行緒。
實施步驟:
- WebForms 非同步頁面
- 實作細節:RegisterAsyncTask 包裝非同步 I/O
- 所需資源:程式碼
- 預估時間:1 小時
- MVC/Web API async/await
- 實作細節:使用 HttpClient.Async、EF Async
- 所需資源:程式碼
- 預估時間:1–2 小時
關鍵程式碼/設定:
// WebForms
PageAsyncTask task = new PageAsyncTask(async ct => {
var a = await CallApiAsync("A");
var b = await CallApiAsync("B");
// 組合結果
});
RegisterAsyncTask(task);
// MVC/Web API
public async Task<ActionResult> Index() {
var a = CallApiAsync("A");
var b = CallApiAsync("B");
await Task.WhenAll(a, b);
return View();
}
實際案例:I/O 併發後延遲下降 實作環境:.NET Framework 4.6+(async/await) 實測數據: 改善前:P95=1200ms 改善後:P95=450ms 改善幅度:-62.5%
Learning Points(學習要點) 核心知識點:
- 非同步 I/O 與請求執行緒釋放
- WebForms PageAsyncTask 與 async/await 技能要求:
- 必備:C# async/await
- 進階:正確錯誤處理與取消 延伸思考:
- 對同步相依程式庫如何包裝?
- async 全鏈路的一致性
Practice Exercise(練習題)
- 基礎:改寫兩個外呼為 Task.WhenAll(30 分鐘)
- 進階:加入取消與逾時(2 小時)
- 專案:將一頁面全面 async 化(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):結果正確
- 程式碼品質(30%):無死結、無同步等待
- 效能優化(20%):可量測
- 創新性(10%):最佳化策略
Case #10: 避免 Task.Result/Wait 在 ASP.NET 造成死結:ConfigureAwait(false)
Problem Statement(問題陳述)
業務場景:引入 async/await 後,部分程式仍用 .Result 或 .Wait(),偶發死結或卡住。 技術挑戰:同步阻塞等待會與 ASP.NET SynchronizationContext 互鎖。 影響範圍:請求逾時、資源浪費。 複雜度評級:中
Root Cause Analysis(根因分析)
直接原因:
- 在有同步內容的環境使用 .Result/.Wait
- await 未使用 ConfigureAwait(false)
- Task 與同步內容互等
深層原因:
- 架構層面:async 混用同步 API
- 技術層面:不了解 SynchronizationContext
- 流程層面:程式碼檢查缺失
Solution Design(解決方案設計)
解決策略:全鏈路 async;避免 .Result/.Wait;在不需要回到內容時使用 ConfigureAwait(false)。
實施步驟:
- 移除同步等待
- 實作細節:將呼叫端改 async,傳遞 async 到邊界
- 所需資源:程式碼
- 預估時間:2–4 小時
- 實施 ConfigureAwait(false)
- 實作細節:庫層 await 一律 ConfigureAwait(false)
- 所需資源:程式碼
- 預估時間:1–2 小時
關鍵程式碼/設定:
// 錯誤:DoAsync().Result;
// 正確:
public async Task<ActionResult> Index() {
var data = await service.GetAsync().ConfigureAwait(false);
// 注意:MVC Action 返回前需切回主執行緒的情況較少
return View(data);
}
實際案例:移除同步等待後逾時消失 實作環境:.NET 4.6+、MVC 實測數據: 改善前:逾時率 2–5%,P95=2s 改善後:逾時率 <0.1%,P95=600ms 改善幅度:>95%
Learning Points(學習要點) 核心知識點:
- SynchronizationContext 與死結
- ConfigureAwait 的使用場景 技能要求:
- 必備:async/await
- 進階:程式碼審查/靜態分析 延伸思考:
- ASP.NET Core 無同步內容,何時仍會卡住?
- 邊界層何時需要 ConfigureAwait(true)?
Practice Exercise(練習題)
- 基礎:移除 .Result/.Wait(30 分鐘)
- 進階:對庫層套用 ConfigureAwait(false)(2 小時)
- 專案:建立 Roslyn Analyzer 驗證(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):無死結
- 程式碼品質(30%):一致 async
- 效能優化(20%):延遲下降
- 創新性(10%):自動化檢查
Case #11: 靜態共享資源競態與鎖:用 ConcurrentDictionary/ReaderWriterLockSlim
Problem Statement(問題陳述)
業務場景:以 static Dictionary 快取資料,偶發 KeyNotFound、死鎖與效能不穩。 技術挑戰:手寫鎖容易造成死鎖或高爭用。 影響範圍:穩定性、效能。 複雜度評級:中
Root Cause Analysis(根因分析)
直接原因:
- 未同步的讀寫競態
- 粗粒度 lock 導致爭用
- 鎖住 this 或字串等錯誤用法
深層原因:
- 架構層面:全域快取無策略
- 技術層面:並行容器不熟悉
- 流程層面:欠缺壓測與競態測試
Solution Design(解決方案設計)
解決策略:改用 ConcurrentDictionary 或 ReaderWriterLockSlim,並建立快取過期與填充策略。
實施步驟:
- 改為並發容器
- 實作細節:GetOrAdd、AddOrUpdate
- 所需資源:程式碼
- 預估時間:1 小時
- 補上過期策略
- 實作細節:MemoryCache + Lazy
- 所需資源:程式碼
- 預估時間:2 小時
- 實作細節:MemoryCache + Lazy
關鍵程式碼/設定:
private static readonly ConcurrentDictionary<string, Lazy<Item>> Cache =
new ConcurrentDictionary<string, Lazy<Item>>();
public Item GetItem(string key) =>
Cache.GetOrAdd(key, _ => new Lazy<Item>(() => LoadFromDb(), LazyThreadSafetyMode.ExecutionAndPublication))
.Value;
實際案例:在高併發查詢下不再出現競態錯誤 實作環境:.NET Framework 4+ 實測數據: 改善前:錯誤率 0.5%,P95=900ms 改善後:錯誤 0%,P95=500ms 改善幅度:錯誤 -100%,延遲 -44%
Learning Points(學習要點) 核心知識點:
- 並發容器與 Lazy 初始化
- ReaderWriterLockSlim 使用場景 技能要求:
- 必備:C#
- 進階:快取策略設計 延伸思考:
- 需要跨節點一致性時如何處理?
- 熱點 key 造成爭用的緩解手法?
Practice Exercise(練習題)
- 基礎:將 Dictionary 改為 ConcurrentDictionary(30 分鐘)
- 進階:Lazy + MemoryCache 過期策略(2 小時)
- 專案:設計快取層與指標監控(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):資料準確
- 程式碼品質(30%):無鎖誤用
- 效能優化(20%):爭用降低
- 創新性(10%):快取預熱
Case #12: 背景執行緒的文化設定與語系不一致
Problem Statement(問題陳述)
業務場景:背景產生報表的日期/貨幣格式與前端顯示不同,引發使用者困惑。 技術挑戰:背景執行緒未繼承請求的 Culture。 影響範圍:格式錯誤、國際化問題。 複雜度評級:低
Root Cause Analysis(根因分析)
直接原因:
- 背景執行緒 Culture 與 UI Culture 為預設
- 依賴 Thread.CurrentThread.CurrentCulture
- 未顯式傳遞
深層原因:
- 架構層面:資料與格式耦合
- 技術層面:不了解文化設定傳遞
- 流程層面:缺少 i18n 測試
Solution Design(解決方案設計)
解決策略:在請求端捕捉 CultureInfo,傳遞到背景工作,執行時設定 Thread.CurrentThread 的 Culture 與 UICulture。
實施步驟:
- 捕捉與傳遞
- 實作細節:封裝工作參數包含 CultureName
- 所需資源:程式碼
- 預估時間:30 分鐘
- 執行時套用
- 實作細節:設定 Thread.CurrentThread.CurrentCulture/UICulture
- 所需資源:程式碼
- 預估時間:30 分鐘
關鍵程式碼/設定:
var culture = System.Threading.Thread.CurrentThread.CurrentCulture;
ThreadPool.QueueUserWorkItem(_ =>
{
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
GenerateReport();
});
實際案例:報表日期格式與 UI 一致 實作環境:多語系網站 實測數據: 改善前:格式錯誤率 10% 改善後:0% 改善幅度:-100%
Learning Points(學習要點) 核心知識點:
- Culture/UICulture
- 背景工作上下文傳遞 技能要求:
- 必備:C#
- 進階:i18n 測試 延伸思考:
- 優先存儲標準格式(ISO 8601),顯示層再轉換
- 多租戶文化設定管理
Practice Exercise(練習題)
- 基礎:傳遞並套用 Culture(30 分鐘)
- 進階:多語系單元測試(2 小時)
- 專案:i18n 策略文件(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):格式一致
- 程式碼品質(30%):清晰傳遞
- 效能優化(20%):無額外壓力
- 創新性(10%):抽象封裝
Case #13: 背景執行緒誤用 HttpContext 導致例外:改傳遞純資料
Problem Statement(問題陳述)
業務場景:背景任務嘗試讀取 HttpContext.Current.Session 或 Request 導致 NullReference 或 ObjectDisposed。 技術挑戰:請求結束後 HttpContext 不保證存在。 影響範圍:任務失敗、不可預期行為。 複雜度評級:中
Root Cause Analysis(根因分析)
直接原因:
- 背景任務直接使用 HttpContext
- 請求完成即釋放相關物件
- 未傳遞必要資料
深層原因:
- 架構層面:邏輯未與 Web 分層
- 技術層面:上下文生命週期不清
- 流程層面:缺少邊界檢查
Solution Design(解決方案設計)
解決策略:在請求端萃取必要資料(如 UserId、表單值),封裝成 DTO 傳入背景工作,任何 Web 物件不得跨越請求邊界。
實施步驟:
- 定義工作 DTO
- 實作細節:只包含序列化資料
- 所需資源:程式碼
- 預估時間:1 小時
- 重構背景邏輯
- 實作細節:移除 HttpContext 依賴
- 所需資源:程式碼
- 預估時間:2–4 小時
關鍵程式碼/設定:
public class ReportArgs { public int UserId; public string Locale; /*...*/ }
var args = new ReportArgs { UserId = GetUserId(), Locale = CultureInfo.CurrentCulture.Name };
QueueBackgroundWork(args);
void QueueBackgroundWork(ReportArgs args)
{
ThreadPool.QueueUserWorkItem(_ => ReportService.Run(args));
}
實際案例:任務穩定執行率提升 實作環境:IIS、WebForms/MVC 實測數據: 改善前:背景任務失敗率 3% 改善後:<0.1% 改善幅度:>96%
Learning Points(學習要點) 核心知識點:
- HttpContext 邊界
- DTO 與層次化設計 技能要求:
- 必備:C#、OO 設計
- 進階:領域層抽離 延伸思考:
- 可否使用 CQRS/消息驅動?
- 事件溯源 + 背景處理的可靠性
Practice Exercise(練習題)
- 基礎:設計 ReportArgs 並移除 HttpContext(30 分鐘)
- 進階:將服務層完全無 Web 依賴(2 小時)
- 專案:CQRS 入門改造(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):任務可執行
- 程式碼品質(30%):分層清晰
- 效能優化(20%):穩定性提升
- 創新性(10%):事件化設計
Case #14: 可取消的長任務:導入 CancellationToken
Problem Statement(問題陳述)
業務場景:使用者誤觸發重任務,希望取消;現有程式無法干預。 技術挑戰:缺少可取消設計。 影響範圍:資源浪費、排程擁塞。 複雜度評級:中
Root Cause Analysis(根因分析)
直接原因:
- 無取消機制
- 無檢查點與中斷點
- 無 UI 暴露取消操作
深層原因:
- 架構層面:單一流程,無控制面
- 技術層面:缺乏 token 傳遞
- 流程層面:沒有使用者控制
Solution Design(解決方案設計)
解決策略:以 CancellationTokenSource 管理可取消任務,在迴圈/步驟加入檢查;UI 提供取消 API。
實施步驟:
- 任務改造
- 實作細節:方法簽章加 CancellationToken,內部檢查
- 所需資源:程式碼
- 預估時間:2 小時
- 前端取消
- 實作細節:提供取消端點與回饋
- 所需資源:Web API
- 預估時間:1 小時
關鍵程式碼/設定:
public Task RunAsync(CancellationToken ct) =>
Task.Run(() => {
foreach (var batch in batches)
{
ct.ThrowIfCancellationRequested();
Process(batch);
}
}, ct);
實際案例:使用者取消後資源回收迅速 實作環境:ASP.NET + Web API 實測數據: 改善前:平均浪費 CPU 60s/次 改善後:<2s 停止 改善幅度:-96%
Learning Points(學習要點) 核心知識點:
- CancellationToken 模式
- 可中斷設計 技能要求:
- 必備:C# Task
- 進階:UI/UX 交互 延伸思考:
- 多租戶的取消權限與風險
- 取消後的補償機制
Practice Exercise(練習題)
- 基礎:把迴圈任務可取消(30 分鐘)
- 進階:提供取消 API 與回饋(2 小時)
- 專案:完整任務生命週期管理(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):可取消
- 程式碼品質(30%):清楚傳遞 token
- 效能優化(20%):立即回收
- 創新性(10%):UX 設計
Case #15: 測試結果受快取影響:控制快取與暖身流程
Problem Statement(問題陳述)
業務場景:同一套測試重跑結果不一致;第一次慢、之後快。 技術挑戰:未區分快取命中與冷啟動。 影響範圍:誤判效能。 複雜度評級:低
Root Cause Analysis(根因分析)
直接原因:
- ASP.NET 快取/資料庫快取預熱差異
- JIT/檔案 I/O 冷啟動
- 壓測未分離冷/熱階段
深層原因:
- 架構層面:無快取策略可觀測
- 技術層面:對 JIT 與快取不敏感
- 流程層面:壓測流程不完善
Solution Design(解決方案設計)
解決策略:建立暖身步驟、控制快取鍵或禁用快取的測試回合;分別量測 cold/warm 指標。
實施步驟:
- 暖身腳本
- 實作細節:先跑固定用例數量
- 所需資源:JMeter/K6
- 預估時間:1 小時
- 快取控制
- 實作細節:在壓測中加上 cache-buster 或直接禁用
- 所需資源:程式碼/設定
- 預估時間:1 小時
關鍵程式碼/設定:
// 控制端點禁用快取(測試用)
[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public ActionResult TestEndpoint() { /* ... */ }
實際案例:冷/熱數據區分後結果穩定 實作環境:IIS、MVC 實測數據: 改善前:重跑 P95 變異 ±40% 改善後:變異 ±5% 改善幅度:波動下降 ~87%
Learning Points(學習要點) 核心知識點:
- 冷啟動與快取影響
- 壓測方法學 技能要求:
- 必備:壓測工具
- 進階:快取設計 延伸思考:
- 生產環境的快取預熱策略
- 避免用測試專用行為污染正式環境
Practice Exercise(練習題)
- 基礎:加入暖身步驟(30 分鐘)
- 進階:測試禁用快取與快取命中兩種情境(2 小時)
- 專案:產生壓測報告模板(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):冷/熱數據可得
- 程式碼品質(30%):測試隔離
- 效能優化(20%):數據穩定
- 創新性(10%):報告自動化
Case #16: IIS 佇列與併發限制導致延遲:調整 queueLength 與並行設定
Problem Statement(問題陳述)
業務場景:壓測時延遲突增、IIS 日誌顯示請求排隊。 技術挑戰:IIS 應用程式集區佇列與 ASP.NET 併發限制配置不當。 影響範圍:吞吐降低、逾時升高。 複雜度評級:中
Root Cause Analysis(根因分析)
直接原因:
- Application Pool queueLength 過小
- ASP.NET 限制 maxConcurrentRequestsPerCPU(舊版)或未調整 minFreeThreads
- 工作進程數與 CPU 綁定不合理
深層原因:
- 架構層面:部署配置未與容量對應
- 技術層面:IIS/aspnet.config 參數不熟
- 流程層面:缺少容量基準
Solution Design(解決方案設計)
解決策略:提高 AppPool queueLength,更新 ASP.NET 併發設定(依版本);監控排隊長度與拒絕率,逐步調參。
實施步驟:
- 調整 AppPool queueLength
- 實作細節:applicationPoolDefaults.queueLength 或個別池
- 所需資源:IIS 管理
- 預估時間:30 分鐘
- 調整 ASP.NET(.NET 4 前後差異)
- 實作細節:.NET 4 前可用 processModel 設定,或 machine.config/aspnet.config 參數;新版本多依賴系統預設
- 所需資源:設定檔
- 預估時間:1 小時
關鍵程式碼/設定:
<!-- applicationHost.config -->
<applicationPools>
<add name="MyAppPool" queueLength="10000" />
</applicationPools>
<!-- aspnet.config(舊版)示例,視版本差異調整 -->
<system.web>
<applicationPool maxConcurrentRequestsPerCPU="5000" requestQueueLimit="5000" />
</system.web>
實際案例:排隊尖峰時吞吐穩定 實作環境:IIS 10、.NET Framework 4.7.2 實測數據: 改善前:排隊>2000、P95=2500ms 改善後:排隊<200、P95=900ms 改善幅度:延遲 -64%
Learning Points(學習要點) 核心知識點:
- IIS queue 與 ASP.NET 併發
- 不同 .NET 版本的併發模型 技能要求:
- 必備:IIS 管理
- 進階:容量規劃 延伸思考:
- 多實例水平擴展 vs 單點調參
- 反向代理的佇列管理
Practice Exercise(練習題)
- 基礎:調整 queueLength 並觀察(30 分鐘)
- 進階:壓測不同參數組合(2 小時)
- 專案:容量與參數建議報告(8 小時)
Assessment Criteria(評估標準)
- 功能完整性(40%):排隊可控
- 程式碼品質(30%):配置管理可追蹤
- 效能優化(20%):指標改善
- 創新性(10%):自動化調參
案例分類 1) 按難度分類
- 入門級:Case 1, 6, 12, 15
- 中級:Case 2, 3, 4, 7, 8, 9, 10, 11, 16
- 高級:Case 5, 8(分散式排程面向)
2) 按技術領域分類
- 架構設計類:Case 3, 4, 5, 8, 11, 13, 14, 16
- 效能優化類:Case 1, 2, 5, 7, 9, 10, 11, 15, 16
- 整合開發類:Case 2, 5, 8, 9, 14
- 除錯診斷類:Case 6, 7, 10, 11, 13, 15, 16
- 安全防護類:本批未聚焦安全,但 Case 6 涉及正確回應終止(間接降低例外外洩風險)
3) 按學習目標分類
- 概念理解型:Case 1, 2, 6, 12, 15
- 技能練習型:Case 7, 9, 10, 11, 14
- 問題解決型:Case 3, 4, 5, 8, 13, 16
- 創新應用型:Case 5, 8, 14(可延伸到雲原生/分散式)
案例關聯圖(學習路徑建議)
- 建議先學:Case 1(測試環境校準)與 Case 15(快取與暖身),避免一開始即被環境因素誤導。
- 之後學習前端/服務端併發上限:Case 2(服務端外呼)、Case 16(IIS 併發與佇列)。
- 基礎管線與常見陷阱:Case 6(Response.End)、Case 7(Session 鎖)。
- 非同步與併發核心:Case 9(非同步 I/O)、Case 10(避免死結)。
- 背景作業可靠性路徑:Case 3(避免手動 Thread)→ Case 4(優雅停止)→ Case 14(可取消)→ Case 8(排程/分散式)。
- 資料與共享狀態:Case 11(並發容器)→ Case 12(文化設定)→ Case 13(移除 HttpContext 依賴)。
- 高級性能/容量:Case 5(CPU 任務外移)收尾強化整體架構。
依賴關係簡述:
- Case 3 依賴 Case 6/13 的概念(正確終止與上下文邊界)
- Case 4 依賴 Case 3(受控背景工作)
- Case 8 依賴 Case 4(生命週期)與 Case 11(併發控制)
- Case 10 依賴 Case 9(先有 async 基礎)
- Case 5 受益於 Case 2/16(併發與容量)與 Case 8(排程)
完整學習路徑: Case 1 → Case 15 → Case 2 → Case 16 → Case 6 → Case 7 → Case 9 → Case 10 → Case 3 → Case 4 → Case 14 → Case 8 → Case 11 → Case 12 → Case 13 → Case 5
說明
- Case 1 的註冊機碼片段與測試注意事項直接來源於原文。
- 其餘案例為基於原文主題「執行緒與 ASP.NET 搭配」的系統化教學擴展,覆蓋該主題在實務中最常見且可量測的問題與解法,便於讀者用原文提供的範例與工具進行實作與驗證。