這個做法講解非同步方法在某些情況下要避免新增執行緒以及避免 Context Switches 來提升程式性能。
許多人誤以為非同步工作必須建立新的執行緒來完成實際工作,例如檔案 I/O 操作使用的就是 IO completion ports 網路請求是使用 network interrupts 它們都不是啟動新的執行緒來完成工作的,在這種情況下使用非同步設計可以把執行緒挪去做更有用的任務。
首先要了解把一個工作轉移到另一個新的執行緒上,雖然原本執行緒的壓力會減輕但同時又要額外負擔建立執行緒的開銷, 也就是說這個減輕壓力的執行緒需要是一個很重要的資源否則這個減輕過程就沒有意義,例如 GUI 上的 UI Thread 就有這樣的特性, 因為 UI Thread 一但被長時間阻塞就會讓使用者體驗變得很差,所以這種情況就適合把工作轉移到另一個執行緒上,讓 UI Thread 專門處理使用者互動。
在 Console 應用程式就不太一樣了,假如它也在運行一個 CPU bound 工作,那麼 UI Thread 有沒有被阻塞其實也不太重要, 換句話說這種情況把任務轉移到另一個新的執行緒上並沒有太大好處,因為 UI Thread 也是在旁邊看著負責工作的執行緒完成手中的工作, 所以代表你雖然有兩個執行緒但效果跟一個執行緒差不多,不過如果你的 Console 應用程式需要同時執行多個 CPU bound 工作那麼把這些 工作分散到多個執行緒上也是合理的設計,這個主題會在未來提到。
接下來討論一下 Web 應用程式,下面這段程式把 CPU bound 工作轉移到新的執行緒上,你可能會認為透過這樣的設計可以分散壓力並幫助應用程式 承受更多的請求。
public async Task<IActionResult> Compose()
{
var model = await LongRunningCPUTask();
return View(model);
}
要了解這個設計的問題需要先了解整體流程,首先執行這段方法會先前往 thread pool 取得或創建一條新的執行緒, 這樣原本的執行緒就變得沒有事情做了所以會處於可以被回收的狀態,但是為了讓非同步方法能夠被暫停與恢復,所以背後需要追蹤這個請求的所有狀態, 最後就導致整個方法結束時你產生了兩個 Context Switches 並且沒有任何帶來實質的好處。
所以如果你的目的是要讓應用程式能承受更多請求,那應該把這個 CPU bound 工作轉移到另一個應用程式的 process 或者是另一台機器上,才能避免你的 Web 應用程式 被過多無用的執行緒佔據請求資源。
Summary
這個做法解釋非同步方法首先要確保轉移工作到另一個新的執行緒是有意義的,如果操作不當就有可能產生無意義的 Context Switches 並拖慢應用程式的性能, 所以要確定資源是重要的,例如 UI Thread 或稀缺資源才有保留的意義。