非同步Asynchronous | 如何優化 I/O Bound 和 CPU Bound

  1. 非同步是什麼? 同步的世界又會長什麼樣子呢?
  2. I/O Bound 和 CPU Bound
  3. async 與 await ( 用C#說明 )
  4. Task 與 Parallel ( 用C#說明 )
  5. 最後會補充:JavaScript 如何成為非阻塞性 I/O 模型 ( Non-blocking I/O model )的非同步 ( asynchronous ) 語言

說明 非同步 前,先讓大家了解「同步」是什麼⬇️

我們來想像一個「同步」的世界,一項事情完成才能再做下一項。

學習寫程式的我們將完成的作業 Mail 給老師批改,

老師改一份作業大概要 1 小時,

將作業交給老師後,
你只能在電腦前不做其他事情地等 1 小時老師改完回覆給你…

你可能很驚訝,等待時不能做其他事嗎!?

沒錯,你只能等…因為這是同步的世界,… 終於等到老師 Mail 回覆你了!

收到了,但等一小時有點久於是你去旁邊舒緩筋骨,再來打開老師的 Mail 。

老師點出作業的錯誤,你花了 1 小時修正後,再次 Mail 出…

而你還是不做其他事地等老師回覆…

可能等 30 分鐘後,老師回覆你 "作業沒問題了!",終於可以做其他事了……。

這就是同步的世界!

也可以看這個影片的前 4 分鐘<AWS re:Invent 2022 - Keynote with Dr. Werner Vogels>,了解如果世界是同步的,會是什麼樣子😂
(如果世界是同步的,薯條一次只能炸一根…😱)

 

你肯定覺得很沒效率,等待的時間明明可以做其他事,老師回覆了再來處理就好。

像是可以拿來刷 Leetcode、看幾個線上課程、做 side project …,

等待時邊做其他事,就可以在更短的時間內學到更多內容、更有效率。

這就是非同步的重要性!

在程式世界中,非同步是什麼?

非同步在程式世界中,

就是允許程式在等待長時間的操作(例如:網絡請求等外部資源處理完的回傳)時,繼續執行其他任務。

這避免了程序在等待操作完成時阻塞或停止執行。

像是 API 打出去,等待的期間先做其他事情,API 回傳時再回來接下去做。

所以「非同步」,比「同步」單純等待,是更有效率,讓資源有效利用

執行程式時,有哪些需要等待?

第一種,I / O 密集型(I / O Bound)

I / O
是 Input / Output(輸入/ 輸出)的操作。指數據在系統內部(處理器和記憶體)與外部存儲設備(硬碟 HDD、固態硬碟 SSD 等)、網路……之間的傳輸。

I / O 密集型
是指系統運作的瓶頸主要在於 I / O 的操作速度,導致 CPU 處於閒置狀態(CPU占用率相對較低),等待 I / O 操作完成。

以下是 I / O 密集型 系統工作或應用:

  • 文件系統操作:讀寫文件、搜尋文件、目錄管理等。
  • 資料庫操作:資料的新增、查詢、更新、刪除等。
  • 網絡數據的發送和接收:Web 伺服器處理 HTTP 請求(打 API)、郵件伺服器處理郵件傳輸等。

第二種,計算密集型(CPU Bound)

CPU(Central Processing Unit,中央處理單元)
是電腦的主要硬件之一,負責執行計算(算術運算、數據傳輸、邏輯操作等)。

CPU 密集型
是指系統運作的瓶頸主要在於 CPU 處理速度,而不是等待 I / O 操作或其他外部因素。

以下是 CPU 密集型 系統工作或應用:

  • 複雜的數學計算
  • 大規模數據分析
  • 大規模資料庫操作
  • 圖像處理
  • 機器學習和AI人工智能

這些應用主要涉及大量的計算,需要消耗大量的CPU資源。

 

辨別是 I /O Bound 還是 CPU Bound 後,再對症下藥進行優化。

如何優化 I / O 密集型的問題?

可以想成一位廚師有炸薯條和做漢堡兩件事情,
如果光是等待薯條油炸,或是只等漢堡排煎熟,沒在等待時做其他事,就形成了 I / O Bound 情境。

解決 I / O Bound 的方式 → 就是等待時先做其他事。

在等待薯條油炸時,可以洗生菜切生菜做漢堡的備料,煎漢堡排時,可以洗馬鈴薯切馬鈴薯做薯條的備料……。
事情的處理過程就會像下圖,交錯做兩件事情、等待時就處理其他任務。

不同顏色代表不同的事情。

在程式世界中,C#語言是用async/await釋放 I /O 等待期間的執行緒 ( thread ) ,以便它可以用於其他任務。

在JavaScript的執行環境Node.js中使用asyncPromise,在JavaScript中避免使用回調地獄(callback hell)。

這邊主要說明C#語言的async/await

async

  • async關鍵字用於標記一個方法為非同步方法。非同步方法通常返回TaskTask<T>類型。
  • 使用async標記的非同步方法,允許在該方法內部使用await關鍵字等待其他非同步操作的完成。
  • 非同步方法不會在每個await表達式處阻塞執行,而是在等待非同步操作完成時釋放執行緒,使執行緒可以去執行其他工作。

await

  • await關鍵字用於等待非同步操作的完成。它只能在標記為async的方法內部使用。
  • 使用await時,當前方法的執行會在該點暫停,當前執行緒會被釋放去執行其他任務。
  • 直到等待的非同步操作完成後,會再拿執行緒從暫停點繼續執行該方法(可能是原來的執行緒,也可能是另一個執行緒)
  • await表達式的結果是被等待非同步操作的返回值(如果有的話)。

使用非同步方法可以使 CPU 在等待 I /O 操作完成時處理其他任務,進而提高整體效率。

非阻塞執行 ( Non-Blocking ) :async/await以非阻塞的方式執行 I/O 操作,等待 I/O 操作完成時,執行緒可以繼續處理其他工作。

如何優化 CPU 密集型的問題?

若是 CPU Bound

執行緒不會遇到等待的情況,只是執行緒要花時間計算。

優化方式:

1.改變演算法:尋求更高效的算法來減少計算量。

寫程式我們常用"迴圈",請程式碼一圈圈幫我們跑。1~數字N,當N太大時,就會遇到伺服器用大量的記憶體循環計算而導致記憶體不足的錯誤。

但其實我們可能只要用一行等差數列求和N(1+N)/2的數學公式就解決了!電腦只需要跑這行公式就能輸出結果,避免迴圈太多次堵塞CPU,之後數字N多大都沒問題了!

(真的不要寫程式寫到,忘記我們國小就會的公式,感謝數學系同學幫我突破盲點🤣)

2.利用多核心處理器用多執行緒技術來利用多核CPU的計算能力,通過平行處理來加速CPU密集型任務。

用多執行緒,就像是軟體工程師要知道五個重要知識,五個同學一人做一個主題再分享學習,比起一個人自己找五個主題的資料,學習更快速 😂

假設,一個執行緒執行一千萬筆的資料共10秒,將資料拆一半成各五百萬筆的資料,叫兩個執行緒同時執行五萬筆資料,可能只要5秒。

一個人處理一千萬筆的資料

 

兩個人一起處理一千萬筆的資料

這邊主要用C#語言說明

當遇到CPU密集型問題時,async/await並不是好的選擇:

  • 不釋放CPU資源async/await主要是釋放I/O等待期間的執行緒去用於其他任務。對於CPU密集型任務,使用async/await並不會釋放CPU資源,CPU仍然需要執行計算工作。
  • 無法利用多核心async/await並不會自動將工作分配到多個核心上,因此對於需要平行處理的CPU密集型任務,它無法充分利用多核心處理器的計算能力。

CPU密集型問題,應使用平行處理技術,如在.NET中使用TaskParallel,可以將計算工作分配到多個核心或執行緒上,從而提高計算效率和性能。

Task

  • 非同步性Task是輕量級的物件,用於表示非同步操作。Task可以在不阻塞主執行緒的情況下繼續執行,不僅限於 CPU 密集型任務,也可用於 I/O 操作。
    在某些情況下,可能需要結合使用async/awaitTask,例如,要在非同步操作後執行後續計算時,可以使用Task來封裝這些計算。
  • 可等待性Task支援非阻塞的等待(使用await關鍵字),這允許程序在Task完成前在非同步方法中暫停和恢復執行,這有助於保持應用程序的響應性。
  • 返回值:使用Task<T>可以返回操作的結果,這讓非同步方法可以像同步方法一樣返回值。

Parallel 平行

  • 簡化多執行緒Parallel提供了簡單的API來執行並行操作,如Parallel.ForParallel.ForEach,用在多核心處理器上平行處理迴圈的迭代。
  • 自動執行緒管理Parallel的方法自動處理執行緒的創建和管理,自動將工作分散到所有可用的CPU核心上,減少任務的完成時間。
  • 適用於數據並行任務:當你有大量相似的任務需要處理,且每個任務的處理時間相對較短時,合適使用Parallel

補充:JavaScript 如何成為非阻塞性 I/O 模型 ( Non-blocking I/O model )的非同步 ( asynchronous ) 語言

JavaScript 是單執行緒,本質上只能一次做一件事情。 

但 JavaScript 搭配 Stack 和 Queue 做到 Event Loop 事件迴圈。

Stack 是執行緒執行的地方 (  Stack 特性為先進後出 )
Queue 是排隊等待放進 Stack 執行的地方 ( Queue 特性為先進先出 )
Event Loop 就是 Stack 的事情處理完,接著從 Queu 拿事情出來一個個處理。 

JavaScript 的 Promise (需要等待的操作會用 Promise 來處理) 和 Callback Function 就會放進 Queue 裡面等待執行。
這個放進 Queue 裡面的行為就讓我們產生 JavaScript 能一次做很多事情的錯覺。 

推薦可以看影片更了解:JavaScript Event Loop解說:單執行緒還能非同步運算? | 走歪的工程師James 

因此 JavaScript 是單執行緒用 Event Loop 達到非阻塞性 I/O 模型 ( Non-blocking I/O model ) 的非同步 ( asynchronous ) 語言

  • 擅長:處理 I / O 密集型達到非同步的效果。 
  • 不擅長: 
    1. CPU 密集型,JavaScript 因為單執行緒無法用多執行緒解決。 
    2. 當某個請求處理時間過長時,JavaScript 因為單執行緒還是會導致整體性能下降。 

而Node.js,就是 Javascript 後端執行環境 ( runtime environment ),建立在單執行緒的 Chrome V8 JavaScript 引擎上。

因此 Node.js 也是以單執行緒、Event Loop 方式、非阻塞性 I/O 模型 ( Non-blocking I/O model )、非同步 ( asynchronous ) 的執行JavaScript 程式語言。 

不像其他程式語言平臺會為每個新請求生成一個額外的執行緒,導致佔用大量記憶體並需要更長的處理時間。

Node.js 可以處理數以萬計的同時連接而不至於因執行緒管理或記憶體消耗而負荷過重。

透過 Event Loop 與 Callback Function,Node.js 能有效地分配系統資源,提供高效能與可擴展性。

非常適合即時通訊服務、線上遊戲、串流平台等,例如 Netflix、Uber、PayPal 和 NASA 等都在他們的生產環境中使用 Node.js。

 Node 和 V8 都經常進行更新、優化性能和安全性、支援現代 JavaScript 功能。

 

參考

Huanlin ─ async 與 await

JavaScript Event Loop解說:單執行緒還能非同步運算? | 走歪的工程師James 

C# 非同步程序設計案例

.Net 非同步工作的延續

AWS re:Invent 2022 - Keynote with Dr. Werner Vogels

謝謝觀看,此為新手的學習筆記整理,若有錯誤,煩請指正🙏