I/O 多工(I/O multiplexing)
想像一位服務生負責二十張桌子。他不能僵在一張桌前、等那桌客人拿主意,任憑其他桌揮手要服務。他要掃視整個用餐區,只走向此刻有需要的桌子。I/O 多工就是程式版的「掃視全場」能力:讓一個執行緒同時盯住許多通訊端,並被告知哪些已就緒可讀或可寫,於是它永不為單一條安靜的連線而阻塞。
具體來說,你把通訊端設成非阻塞,交給作業系統一份它們的清單,然後呼叫一個多工函式:經典的 select 或 poll,或現代 Linux 上的 epoll、BSD/macOS 上的 kqueue。那個呼叫對整組通訊端阻塞一次,並在組中任何一個變就緒的瞬間回傳——傾聽通訊端有新連線可 accept、用戶端通訊端有位元組可 recv、或某通訊端的傳送緩衝區有空間。你的程式碼接著只對那些就緒的通訊端迴圈,對每個做一個快速的非阻塞操作,然後回去等待。這個單執行緒迴圈叫做事件迴圈,它回應每個就緒通訊端所做的工作則是事件處理常式(event handler)。
為什麼重要:I/O 多工是讓單一執行緒能服務數萬條連線(著名的 C10k 問題)、又不必為每個用戶端生出一個執行緒或行程的方式。它驅動高效能伺服器(Nginx、Redis)以及 Node.js 與 asyncio 的執行期。誠實的取捨:這種程式碼比直線式阻塞程式碼難寫,因為邏輯被拆散到回呼或協程裡;而一個不小心放進迴圈裡的緩慢阻塞操作(同步磁碟讀取、DNS 查詢)會一次卡住每一條連線。多工只告訴你某通訊端就緒了;你仍得正確處理部分讀取與部分寫入。
一個事件迴圈:ready = select(all_sockets);for each s in ready { 若 s 是傾聽者,就 accept 一個新用戶端;否則從 s recv 並處理它 }。一個執行緒、許多通訊端,而且它只碰此刻有事要做的那些。
一個執行緒盯住許多通訊端,只對就緒的那些動作。
就緒不等於做完工作:當迴圈說某通訊端可讀,你仍得做一次可能只回傳部分資料的非阻塞 recv。而任何溜進迴圈裡的阻塞呼叫,會凍結每一條連線,不只一條。