為什麼單執行緒可以處理幾萬個連線?淺談 Event Loop 與 Async/Await 的底層魔法

如果你有寫過 Node.js 或 FastAPI,那你對 async 和 await 這兩個單字一定不陌生。我們天天寫、到處加,但你有沒有停下來想過一個非常違反直覺的問題:
「JavaScript (Node.js) 和 Python 的 Event Loop 基本上都是單執行緒 (Single-threaded) 的。一個員工(執行緒)一次只能做一件事,那它到底是怎麼『同時』處理幾萬個使用者的 API 請求的?」
今天我們不看生硬的原始碼,直接用一間「餐廳」來搞懂這個被稱為 非阻塞 I/O (Non-blocking I/O) 與 事件迴圈 (Event Loop) 的黑魔法。
🍔 傳統的多執行緒 (Multi-threading):霸氣但笨重的土豪餐廳
想像一間傳統的餐廳(例如早期的 Java 或 PHP Apache 伺服器)。 這間餐廳的運作模式是:一個服務生(執行緒)專門服務一桌客人。
- 客人 A 點了一碗牛肉麵。
- 服務生把菜單交給廚房後,他就站在廚房門口死死地盯著鍋子等(這叫 Blocking 阻塞)。
- 等了 10 分鐘,麵煮好了,服務生端給客人 A。
- 服務生再去接客人 B 的單。
如果今天同時來了 1 萬個客人怎麼辦?老闆(作業系統)就必須馬上僱用 1 萬個服務生!但每個服務生都要佔用薪水跟空間(記憶體 CPU 資源 Context Switch),到最後餐廳直接擠爆破產(伺服器當機)。
🚀 單執行緒的非阻塞魔法 (Non-blocking):時間管理大師
現在來看看 Node.js 和 FastAPI 這種「單執行緒 + 非阻塞」的餐廳是怎麼運作的。 這間餐廳只有一個超級服務生(Main Thread 主執行緒)。
- 客人 A 點了牛肉麵。
- 服務生把菜單丟給廚房(丟給作業系統底層去處理網路或資料庫 I/O)。
- 重點來了:服務生不傻等! 他轉頭立刻去幫客人 B 點餐。
- 客人 B 點餐完,廚房也把資料庫的查詢搞定了,廚房按了一下鈴(觸發 Event 事件)。
- 服務生聽到鈴聲,過去把牛肉麵端給客人 A。
發現了嗎?這個服務生(主執行緒)從頭到尾都沒有閒下來過,但他卻靠著一個人,同時處理了無數個客人的請求。 這就是單執行緒能撐起萬級併發連線的核心秘密!
⚙️ 打開黑盒子:Event Loop 到底長怎樣?
我們把這個「餐廳模型」對應回程式語言的底層架構,就會看到經典的 Event Loop 機制。它由四個核心元件組成:
-
Call Stack (呼叫堆疊 / 服務生本人): 這是程式執行的地方。裡面永遠只能有一個任務在跑。如果是簡單的數學計算
1 + 1,馬上算完馬上離開。 -
Web APIs / Background Workers (廚房 / 作業系統): 當服務生遇到那種要等很久的任務(例如
去資料庫撈資料、打別人的 API、讀取硬碟檔案),他就會把這個任務踢給背後的 C++ 模組或作業系統底層處理。這時候 Call Stack 就空出來了,可以繼續執行下一行 Code。 -
Task Queue / Callback Queue (出餐台 / 任務佇列): 當廚房(作業系統)把資料庫的資料撈完後,它不能直接把資料塞回 Call Stack(因為服務生可能正在忙別桌)。所以它會把打包好的回呼函式(Callback)放在「出餐台」排隊等待。
-
Event Loop (事件迴圈 / 大堂經理): 這是一個永遠在轉圈圈的無限迴圈。它唯一的工作就是:緊盯著 Call Stack 看。只要發現 Call Stack 空了(服務生閒下來了),就把 Task Queue(出餐台)裡排隊的第一個任務推到 Call Stack 裡執行。
🧙♂️ 那 async / await 到底在幹嘛?
早期我們為了接住從出餐台送回來的資料,寫了滿坑滿谷的 Callback 函式(也就是惡名昭彰的 Callback Hell)。後來演進出 Promise,再後來,就是我們現在天天用的 async / await。
await 其實就是一層超美的語法糖。當你在程式碼裡寫下:
# FastAPI 範例
@app.get("/users/{user_id}")
async def get_user(user_id: int):
# 遇到 await,主執行緒說:「好,你去旁邊撈資料,我先去服務別人!」
user_data = await db.fetch_user(user_id)
# 等資料撈完被推回 Call Stack 後,再從這裡繼續往下跑
return {"user": user_data}
await 的本質就是:在這裡暫停這個函式,把控制權還給 Event Loop,讓主執行緒去處理其他 API 請求。 等到底層把 I/O 做完,Event Loop 會再把你叫回來,從暫停的地方繼續往下跑。
⚠️ 避雷針:別在 Event Loop 裡做數學題!
懂了這個原理,你就會明白單執行緒架構最大的一個死穴:CPU 密集型任務。
如果今天某個客人不是點牛肉麵,而是點了一道「請服務生站在桌邊心算圓周率到小數點後一萬位」的菜(例如複雜的影像處理、巨大的 for 迴圈)。
這下完蛋了!服務生(主執行緒)被卡死在 Call Stack 裡,他沒辦法去接單,也聽不到廚房的搖鈴聲。整個伺服器會瞬間失去反應,這就叫做 「阻塞了主執行緒 (Blocking the Main Thread)」。
所以在 Node.js 或是 FastAPI 裡面,永遠不要在主執行緒裡執行大量消耗 CPU 的同步運算。遇到這種任務,乖乖把它丟給多進程(Multiprocessing)或是背景 Worker(如 Celery、Redis Queue)去算就對了。
🍻 總結
總結一句話:單執行緒之所以能同時處理幾萬個連線,是因為它把「等待網路和硬碟的時間」全部榨乾了。
它不浪費任何一毫秒在「發呆等待」上,而是透過 Event Loop 不停地切換任務。這就是現代高效能 Web 框架(FastAPI, Node.js)能夠在輕量級資源下,扛住驚人流量的核心魔法!下次打下 await 的時候,別忘了感謝一下底層那個瘋狂奔跑的大堂經理吧!
