[Migrate .NET] 在 Named Pipe 連線時發現 Timeout 行為的變化

當前手上處理的系統,整體的程式架構概念可以簡化成:

HW/FW ←→ CLI ←→ GUI

 

HW/FW 的程式,大多是透由 C/C++ 撰寫,這部分有專業的人員處理開發。

此系統中的 CLI 與 GUI 這兩部分的程式,則是透由 .NET 開發的。也因此隨著 .NET 的跨平台發展,便得以讓這兩部分的程式,皆可以很有序地從 Windows 轉移至 Linux 當中運作。

而在 CLI ←→ GUI 的資訊交換處理,則有大一部份仰賴著使用 .NET 當中的 Named Pipe

 

當把系統中要依賴那尾大不掉的 .NET Framework 的程式,一步步地搬遷到 .NET 6 並逐步的穩定運作一段時日後,隨著 .NET 6 也已走入歷史

考量了 .NET 的發行步調 後,在現行系統的程式要針對 .NET 下一個版本進行遷移選擇時,最終則是選擇跳過 .NET 8 切入到 .NET 10。

 

當把系統中的各專案的 TFM 切換到 .NET 10 之後,執行時馬上就拋出了如下的錯誤:

Fail to connect the pipe : The operation has timed out.


發生了什麼事?

 

這問題透過現代化的 AI 指引,雖然很快地就被精準的指向是在 NamedPipeClientStream.Connect(timeout)的使用發生了預期外的狀況。

檢視了這段底層 Pipe 運作的程式之後,除了發現本來自己系統上所設計的運作機制不太合理之處,也看到了 .NET Framework /.NET Core 時代,在 Runtime 的設計上本身就有的缺點,直到了 .NET 5 之後才完善實作的部分。 

本篇就先來聊聊後者的這部分。


這部分主要來自於是 Timeout 行為的差異,以下列出幾個 .NET 的改變階段來看:

RuntimeTimeout 行為
.NET Framework 4.x不精準
.NET Core 2.x開始修正
.NET Core 3.x大幅改善
.NET 5接近現代實作
.NET 6已經很準
.NET 8/9/10幾乎相同

 

在這邊就看到了一個分水嶺

.NET Framework / .NET Core / .NET 5 時期:

很多 API 的 timeout 設計並非相當精確,也就是說 timeout 的行為還是會有採取比較模糊的作法。

.NET 6 +,則對於 timeout 的觀點就趨近一個很明確策略規範:

Timeout 是要 deterministic 的

 

所以 .NET 技術團隊在 .NET 6+ 整理完 timeout 行為一致性後,其原則:

timeout = maximum waiting time

而非過去的:

timeout ≈ approximate waiting time

 

在 .NET Core 2.x/3.x 時有兩種改善策略進行重構:

  • 在 API 設計時,皆是採取更精準的剩餘時間計算觀點remaining = timeout - (current - start)
  • 並大幅減少 Thread.Sleep polling 的設計。

到了 .NET 5+ 更是對 timeout accounting 改進移除模糊的 retry 行為

逐步地達成下列三點:

  1. timeout 行為要一致。
    像是 Socket、HttpClient、Task、Semaphore、Pipe … 等,這些的底層類別的運作,timeout 都要一致,讓 timeout = max wait time。
  2. 減少 busy waiting。
    舊版 runtime 的 polling 會造成 latency 不穩 CPU 浪費 的問題
  3. async API 的一致性。
    現在很多 API 都支援 ConnectAsync(CancellationToken),timeout 其實會變成 CancellationTokenSource(timeout)所以 runtime 需要 更 deterministic


因此會發現…

NamedPipeClientStream.Connect(timeout)在從 .NET Framework 升級到 .NET 後出了問題,這是一個 Runtime 行為逐步被修正與一致化 的結果。


所以整體的改善時序與影響會為:

事件大致時間影響
有關 busy wait 問題2016早期 .NET Core retry 行為錯誤導致不準 timeout
深入改善 TryConnect 邏輯2017+修正 retry / WaitNamedPipe ordering
多個修正 / 重構.NET Core 3.x → 5.x逐步統一 timeout accounting
.NET 6+.NET 6 → 10行為趨於精準一致,但非「突變」

 

有興趣的話,可以深入看看官方 GitHub 的 Issues 有的主要相關的討論:

  • Issue #16945 NamedPipeClientStream 早期實作確實有 retry / spin wait 行為。
  • Issue #22678 討論 retry 與 WaitNamedPipe ordering 的 race conditions,顯示舊邏輯不是理想的實作。
  • Issue #65434 顯示以多 client 重現下舊實作仍有邊緣錯誤,後續版本修正一些錯誤處理邏輯。

 

就用這張圖總結觀點:


 


I'm a Microsoft MVP - Developer Technologies (From 2015 ~).
 

MVP_Logo



I focus on the following topics: Xamarin Technology, Azure, Mobile DevOps, and Microsoft EM+S.

If you want to know more about them, welcome to my website:
https://jamestsai.tw 


本部落格文章之圖片相關後製處理皆透過 Techsmith 公司 所贊助其授權使用之 "Snagit" 與 "Snagit Editor" 軟體製作。