[RUN! PC] 2008 四月號

以下內容基於原文主題「執行緒與 ASP.NET 搭配的技術議題」與文中明確提到的瀏覽器連線數限制調校,系統性重構為可教學、可實作、可評估的 16 個案例。每個案例都聚焦於 ASP.NET 與多執行緒/併發實務中常見、可量化與可驗證的問題與解法。若文章未直接給出具體數據,以下實測數據與環境均為可重現的教學性設定與估測,便於練習與評估。

Case #1: 調整 IE 同站連線上限,修正併發測試失真

Problem Statement(問題陳述)

業務場景:團隊要驗證 ASP.NET 範例中的併發下載/請求是否真的平行執行。使用 IE 測試時,發現同時觸發多個請求卻似乎被序列化,併發效益不明顯,導致功能價值被低估,風險是錯判架構可行性。 技術挑戰:IE 預設每站台僅 2 條 HTTP 連線,限制前端併發能力,測試結果失真。 影響範圍:誤判效能、錯估伺服器端資源配置、影響上線決策。 複雜度評級:低

Root Cause Analysis(根因分析)

直接原因:

  1. IE 預設 MaxConnectionsPerServer=2,導致同站發出多請求時被限制。
  2. 測試以同一主機名與 HTTP/1.1 進行,受 per-host cap 影響。
  3. 未使用專業壓測工具,僅靠瀏覽器點擊驗證。 深層原因:
    • 架構層面:將前端併發能力假設為「足夠」,缺乏測試環境基線設定。
    • 技術層面:忽略瀏覽器連線池策略與 HTTP/1.1 行為。
    • 流程層面:缺少「環境校準」步驟與測試指引。

Solution Design(解決方案設計)

解決策略:在測試機上調高 IE 同站連線上限至 8 以上,或改用支援更高並行的瀏覽器/壓測工具;同時以多子網域或不同 host 分散 per-host 限制,確保測試與目標負載相符。

實施步驟:

  1. 調整 IE 註冊機碼
    • 實作細節:設定 MaxConnectionsPerServer 與 MaxConnectionsPer1_0Server 為 8+
    • 所需資源:regedit 或 .reg 檔
    • 預估時間:10 分鐘
  2. 使用專業壓測工具
    • 實作細節:以 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(根因分析)

直接原因:

  1. ServicePointManager.DefaultConnectionLimit 預設為 2
  2. 同站多請求同時發出但被限制為 2 條
  3. 未針對關鍵 ServicePoint 單獨調整 深層原因:
    • 架構層面:集中式 API Gateway 未分流/拆域名
    • 技術層面:忽略 .NET service point 連線池限制
    • 流程層面:缺少啟動時動態配置連線上限

Solution Design(解決方案設計)

解決策略:在應用啟動時提升預設連線上限,或對特定 Uri 的 ServicePoint 設定 ConnectionLimit;同時評估 HTTP/2 支援或分散至多 host。

實施步驟:

  1. 啟動時設定全域上限
    • 實作細節:在 Global.asax Application_Start 設定 DefaultConnectionLimit
    • 所需資源:程式碼修改、重新部署
    • 預估時間:30 分鐘
  2. 針對特定主機微調
    • 實作細節: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(根因分析)

直接原因:

  1. 手動 new Thread 未註冊到 ASP.NET 宿主管理
  2. 未處理停止訊號,無法優雅結束
  3. 任務依賴 HttpContext,請求結束後失效 深層原因:
    • 架構層面:應用層承擔長任務,缺少工作者服務
    • 技術層面:不了解 ASP.NET 執行緒與宿主行為
    • 流程層面:缺乏長任務標準方案

Solution Design(解決方案設計)

解決策略:用 HostingEnvironment.RegisterObject + ThreadPool/Task 執行,或採用排程/背景框架(Hangfire/Quartz),確保受控生命週期與優雅關閉。

實施步驟:

  1. 以 IRegisteredObject 接管生命週期
    • 實作細節:實作 Stop,註冊/註銷,使用 ThreadPool
    • 所需資源:程式碼
    • 預估時間:2 小時
  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(根因分析)

直接原因:

  1. 未實作 IRegisteredObject.Stop
  2. 未保存進度/檢查點
  3. 任務依賴記憶體狀態,回收即遺失 深層原因:
    • 架構層面:應用與批次耦合
    • 技術層面:不了解 IIS 回收流程
    • 流程層面:缺乏恢復策略

Solution Design(解決方案設計)

解決策略:以 IRegisteredObject 訂閱回收通知、持續寫入進度,Stop 時嘗試收尾或持久化,重啟後續跑。

實施步驟:

  1. 實作可中斷作業
    • 實作細節:定期檢查 _stopping,寫入 checkpoint
    • 所需資源:資料庫/檔案儲存
    • 預估時間:4 小時
  2. 啟動時恢復
    • 實作細節: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(根因分析)

直接原因:

  1. CPU-bound 工作於請求執行緒上進行
  2. ThreadPool 擴張延遲,瞬時飢餓
  3. 無隔離資源限制與排程 深層原因:
    • 架構層面:缺乏背景服務/工作佇列
    • 技術層面:誤用 Task.Run 於 ASP.NET
    • 流程層面:未做效能分析與容量規劃

Solution Design(解決方案設計)

解決策略:將 CPU 工作外移到獨立工作者服務(Windows Service/Container)+ 佇列;頁面僅投遞工作並回傳收件編號/輪詢結果,避免阻塞請求執行緒。

實施步驟:

  1. 建立佇列與工作者
    • 實作細節:Hangfire/Quartz/RabbitMQ;限制並行度
    • 所需資源:Hangfire Server 或 MQ
    • 預估時間:1–2 天
  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(根因分析)

直接原因:

  1. Response.End 內部呼叫 Thread.Abort
  2. 異常中斷一般清理邏輯
  3. 訊息與追蹤不一致 深層原因:
    • 架構層面:頁面流程控制不清
    • 技術層面:不了解 Response 管線
    • 流程層面:缺乏統一封裝

Solution Design(解決方案設計)

解決策略:改用 HttpApplication.CompleteRequest,避免 Thread.Abort;將提前返回封裝為共用 helper。

實施步驟:

  1. 封裝提前結束
    • 實作細節:設定狀態碼/標頭/內容後呼叫 CompleteRequest
    • 所需資源:程式碼
    • 預估時間:30 分鐘
  2. 移除 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(根因分析)

直接原因:

  1. 預設 EnableSessionState 開啟且為可寫
  2. 同一使用者請求被 Session 寫鎖序列化
  3. 不必要的 Session 使用 深層原因:
    • 架構層面:將狀態綁在 Session
    • 技術層面:不了解 Session 鎖定行為
    • 流程層面:未區分讀/寫使用場景

Solution Design(解決方案設計)

解決策略:將無需寫入的頁面/Handler 改為 ReadOnly 或停用 Session;需要寫入時縮短臨界區。

實施步驟:

  1. 降權或停用
    • 實作細節:WebForms 設置 EnableSessionState=”ReadOnly”;Handler 使用 IReadOnlySessionState 或移除
    • 所需資源:程式碼
    • 預估時間:1 小時
  2. 縮小寫入範圍
    • 實作細節:僅在必要區塊存取 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(根因分析)

直接原因:

  1. Timer 與宿主生命週期未整合
  2. 多實例部署造成重複觸發
  3. 未實作鎖與領導者選舉 深層原因:
    • 架構層面:將排程內嵌於 Web
    • 技術層面:缺少分散式協調
    • 流程層面:未規劃任務容錯

Solution Design(解決方案設計)

解決策略:以 IRegisteredObject 註冊與管理 Timer,或改採 Quartz.NET/Hangfire(持久化 + 分散式鎖)。

實施步驟:

  1. 宿主註冊模式
    • 實作細節:Stop 時停止 Timer,避免回收期間觸發
    • 所需資源:程式碼
    • 預估時間:2 小時
  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(根因分析)

直接原因:

  1. 同步 I/O 導致請求執行緒閒置等待
  2. ThreadPool 不必要擴張
  3. 未使用非同步模型 深層原因:
    • 架構層面:未分類 I/O vs CPU 工作
    • 技術層面:不了解 ASP.NET 非同步頁面
    • 流程層面:缺少 async 樣板

Solution Design(解決方案設計)

解決策略:WebForms 用 PageAsyncTask,或 MVC/Web API 用 async/await 實作真正非同步 I/O,釋放請求執行緒。

實施步驟:

  1. WebForms 非同步頁面
    • 實作細節:RegisterAsyncTask 包裝非同步 I/O
    • 所需資源:程式碼
    • 預估時間:1 小時
  2. 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(根因分析)

直接原因:

  1. 在有同步內容的環境使用 .Result/.Wait
  2. await 未使用 ConfigureAwait(false)
  3. Task 與同步內容互等 深層原因:
    • 架構層面:async 混用同步 API
    • 技術層面:不了解 SynchronizationContext
    • 流程層面:程式碼檢查缺失

Solution Design(解決方案設計)

解決策略:全鏈路 async;避免 .Result/.Wait;在不需要回到內容時使用 ConfigureAwait(false)。

實施步驟:

  1. 移除同步等待
    • 實作細節:將呼叫端改 async,傳遞 async 到邊界
    • 所需資源:程式碼
    • 預估時間:2–4 小時
  2. 實施 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(根因分析)

直接原因:

  1. 未同步的讀寫競態
  2. 粗粒度 lock 導致爭用
  3. 鎖住 this 或字串等錯誤用法 深層原因:
    • 架構層面:全域快取無策略
    • 技術層面:並行容器不熟悉
    • 流程層面:欠缺壓測與競態測試

Solution Design(解決方案設計)

解決策略:改用 ConcurrentDictionary 或 ReaderWriterLockSlim,並建立快取過期與填充策略。

實施步驟:

  1. 改為並發容器
    • 實作細節:GetOrAdd、AddOrUpdate
    • 所需資源:程式碼
    • 預估時間:1 小時
  2. 補上過期策略
    • 實作細節:MemoryCache + Lazy
    • 所需資源:程式碼
    • 預估時間:2 小時

關鍵程式碼/設定:

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(根因分析)

直接原因:

  1. 背景執行緒 Culture 與 UI Culture 為預設
  2. 依賴 Thread.CurrentThread.CurrentCulture
  3. 未顯式傳遞 深層原因:
    • 架構層面:資料與格式耦合
    • 技術層面:不了解文化設定傳遞
    • 流程層面:缺少 i18n 測試

Solution Design(解決方案設計)

解決策略:在請求端捕捉 CultureInfo,傳遞到背景工作,執行時設定 Thread.CurrentThread 的 Culture 與 UICulture。

實施步驟:

  1. 捕捉與傳遞
    • 實作細節:封裝工作參數包含 CultureName
    • 所需資源:程式碼
    • 預估時間:30 分鐘
  2. 執行時套用
    • 實作細節:設定 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(根因分析)

直接原因:

  1. 背景任務直接使用 HttpContext
  2. 請求完成即釋放相關物件
  3. 未傳遞必要資料 深層原因:
    • 架構層面:邏輯未與 Web 分層
    • 技術層面:上下文生命週期不清
    • 流程層面:缺少邊界檢查

Solution Design(解決方案設計)

解決策略:在請求端萃取必要資料(如 UserId、表單值),封裝成 DTO 傳入背景工作,任何 Web 物件不得跨越請求邊界。

實施步驟:

  1. 定義工作 DTO
    • 實作細節:只包含序列化資料
    • 所需資源:程式碼
    • 預估時間:1 小時
  2. 重構背景邏輯
    • 實作細節:移除 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(根因分析)

直接原因:

  1. 無取消機制
  2. 無檢查點與中斷點
  3. 無 UI 暴露取消操作 深層原因:
    • 架構層面:單一流程,無控制面
    • 技術層面:缺乏 token 傳遞
    • 流程層面:沒有使用者控制

Solution Design(解決方案設計)

解決策略:以 CancellationTokenSource 管理可取消任務,在迴圈/步驟加入檢查;UI 提供取消 API。

實施步驟:

  1. 任務改造
    • 實作細節:方法簽章加 CancellationToken,內部檢查
    • 所需資源:程式碼
    • 預估時間:2 小時
  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(根因分析)

直接原因:

  1. ASP.NET 快取/資料庫快取預熱差異
  2. JIT/檔案 I/O 冷啟動
  3. 壓測未分離冷/熱階段 深層原因:
    • 架構層面:無快取策略可觀測
    • 技術層面:對 JIT 與快取不敏感
    • 流程層面:壓測流程不完善

Solution Design(解決方案設計)

解決策略:建立暖身步驟、控制快取鍵或禁用快取的測試回合;分別量測 cold/warm 指標。

實施步驟:

  1. 暖身腳本
    • 實作細節:先跑固定用例數量
    • 所需資源:JMeter/K6
    • 預估時間:1 小時
  2. 快取控制
    • 實作細節:在壓測中加上 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(根因分析)

直接原因:

  1. Application Pool queueLength 過小
  2. ASP.NET 限制 maxConcurrentRequestsPerCPU(舊版)或未調整 minFreeThreads
  3. 工作進程數與 CPU 綁定不合理 深層原因:
    • 架構層面:部署配置未與容量對應
    • 技術層面:IIS/aspnet.config 參數不熟
    • 流程層面:缺少容量基準

Solution Design(解決方案設計)

解決策略:提高 AppPool queueLength,更新 ASP.NET 併發設定(依版本);監控排隊長度與拒絕率,逐步調參。

實施步驟:

  1. 調整 AppPool queueLength
    • 實作細節:applicationPoolDefaults.queueLength 或個別池
    • 所需資源:IIS 管理
    • 預估時間:30 分鐘
  2. 調整 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 搭配」的系統化教學擴展,覆蓋該主題在實務中最常見且可量測的問題與解法,便於讀者用原文提供的範例與工具進行實作與驗證。





Facebook Pages

AI Synthesis Contents

- 原始文章內容
- 問答集
- 文章摘要
- 解決方案 / Case Study

Edit Post (Pull Request)

Post Directory