前幾天我有一個開發人員挑戰異步服務器端代碼的使用。他問為什么在沒有 UI 執行緒阻塞的服務器上,異步代碼優于同步代碼?我給了他典型的執行緒耗盡答案,但在考慮了一段時間后,我不再確定我的答案是否正確。在做了一些研究之后,我發現作業系統中執行緒的上限是由記憶體而不是任意數字控制的。像 Kestrel 這樣的服務器支持無限執行緒。因此,在“理論上”,服務器可以并行阻塞的請求(執行緒)的數量由記憶體控制。這與 .NET 中的異步代碼沒有什么不同;它將堆疊變數提升到堆,但它仍然受記憶體限制。
我一直認為比我更聰明的人已經考慮過這一點,并且異步代碼是處理 IO 系結代碼的正確方法。但是,當在沒有 UI 執行緒的專用服務器場中運行時,異步 .NET 代碼的顯著優勢是什么?遷移到云 (AWS) 會改變答案嗎?
uj5u.com熱心網友回復:
服務器端異步代碼的目的與異步 UI 代碼完全不同。
異步 UI 代碼使 UI 回應更快(尤其是當多個 CPU 內核可用時),它允許多個 UI 任務并行運行,從而改善 UI 用戶體驗。
另一方面,服務器端異步代碼的目的是盡量減少同時服務多個客戶端所需的資源。事實上,即使只有一個 CPU 內核或像Node.js中那樣的單執行緒事件回圈,它也是有益的。這一切都歸結為一個簡單的
異步 IO概念。
同步和異步 IO 的區別在于,在前者的情況下,初始化 IO 操作的執行緒會暫停,直到 IO 操作完成(例如,直到執行 DB 請求或讀取磁盤上的檔案)。一旦 IO 操作完成以處理它的結果,同一個執行緒就會取消暫停。注意:即使在暫停時執行緒很可能不使用任何 CPU 資源(它可能被執行緒調度程式置于睡眠狀態),它的資源仍然與這個特定的 IO 操作相關聯,并且在 IO 由硬體執行時幾乎被浪費了. 有效地使用同步 IO,每個當前正在處理的客戶端請求至少需要一個執行緒,即使這些執行緒中的大多數可能處于休眠狀態,等待其 IO 操作完成。在 。NET 每個執行緒至少分配了 1MB 的堆疊,因此如果服務器當前正在處理 1000 個請求,則會導致僅為執行緒堆疊分配近 1GB 的記憶體,加上執行緒調度程式的額外負擔以及 CPU 花費更多時間進行背景關系切換:執行緒越多,系統的整體性能就越慢。分配的更多記憶體也意味著記憶體/CPU 快取使用效率更低。
異步 IO 效率更高,因為作業執行緒只初始化一個 IO 操作,而不是等待它完成,而是立即切換到另一個有用的任務(例如繼續另一個客戶端的請求處理),當 IO 操作由硬體完成時,結果的處理在任何可用的作業執行緒上恢復。因此,根據等待硬體完成 IO 所花費的總時間與執行 CPU 任務所花費的時間之間的比率(例如,將 IO 操作的結果序列化為 JSON),這種方法可以使用更少的執行緒服務相同數量的并發客戶端請求:例如,如果 90% 的時間花在 IO 上,我們可能只使用 100 個執行緒來服務相同的 1000 個并發請求。您的服務器端代碼受 IO 限制與受 CPU 限制越多,它可以使用給定數量的資源(CPU 和記憶體)處理的同時客戶端請求越多。
異步代碼的缺點是什么?主要是它通常比同步更難撰寫。異步代碼使用回呼來恢復操作,因此程式員需要將委托(延續)傳遞給 IO 方法,而不是簡單的線性代碼,該方法稍后在 IO 操作完成時由系統呼叫(可能在不同的執行緒上)。然而,現代 C# 及其async/await設施使這項任務變得不那么復雜,甚至使異步代碼幾乎看起來像同步的。唯一要記住的是:異步代碼僅在“一直向下”異步時才有效:即使是單個Task.Wait或Task.Result從初始 HTTP 請求處理到 DB 請求呼叫的呼叫堆疊中的某個位置使整個代碼同步,從而迫使當前作業執行緒等待該Wait呼叫完成破壞目的。注意:await在 C# 中,代碼實際上并不等待呼叫的結果,而是由編譯器轉換為ContinueWith即繼續回呼,盡管實際上它比這要復雜一些,但幸運的是復雜性對程式員來說是隱藏的,所以如今,撰寫高效的異步代碼是相對簡單的任務。
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/462286.html
