阻塞式通訊端(blocking socket)
想像你站在信箱前等一封信,在它到來之前什麼別的事都不肯做——連眨眼都不肯。那就是阻塞行為。預設情況下,通訊端是阻塞的:當你呼叫 recv 而還沒有資料抵達時,這個呼叫就單純地等待,你的程式凍結在那一行、什麼都不做,直到有些位元組出現或連線關閉。同樣地,若核心的傳送緩衝區滿了,阻塞式 send 也會等。
更精確地說,阻塞式呼叫會讓你的執行緒進入睡眠,作業系統只在該操作能有進展時才喚醒它。這在思考上美妙地簡單:你寫直線式的程式碼——connect、送出請求、recv 回應——每一步都先完成才換下一步。代價是一個執行緒一次只能照顧一個通訊端。若你對一條安靜的連線呼叫 recv,那個執行緒就無法同時讀一條繁忙的連線。accept 與 connect 也會阻塞:阻塞式 accept 會睡到有用戶端到來。
為什麼重要:阻塞式通訊端是自然、易讀的預設值,對於跟單一伺服器對話的用戶端,或為每條連線各給一個執行緒或行程的伺服器,它正合適。麻煩出現在一個執行緒必須服務許多連線時:對一個閒置用戶端的單一阻塞 recv,會卡住排在它後面的所有人。解法是並行(每條連線一個執行緒或行程,代價是記憶體與情境切換),或把通訊端切換成非阻塞模式並使用 I/O 多工(select、poll、epoll),讓一個執行緒能盯住許多通訊端、只對就緒的那些動作——這就是 Nginx 等高效能伺服器背後的事件迴圈模型。
一台單執行緒伺服器對用戶端 A 呼叫 recv,而 A 一分鐘都沒送任何東西。執行緒整整睡了一分鐘。準備好對話的用戶端 B 與 C 被無視——因為那唯一的執行緒卡在 A 上。這正是 I/O 多工要解決的陷阱。
易讀,但一個阻塞的呼叫會卡住排在它後面的一切。
阻塞不是壞事——當每條連線都有自己的執行緒或行程時,它是最簡單而正確的模型。只有當你想用單一執行緒、又不做多工,去服務許多連線時,它才成為問題。