提到 IO 模型,同步、異步、阻塞、非阻塞這幾個詞總是繞不開。乍看之下似乎很好理解,但細究起來卻常讓人霧裡看花。筆者也曾多次被這些概念搞得迷迷糊糊,看懂了沒多久又混淆了。經過幾次反覆,發現若不系統總結提煉,很難真正掌握。網路上關於此的文章良莠不齊,錯誤觀點更容易誤導初學者。
最貼切的類比莫過於「去餐廳吃飯」。下文將結合這一生活場景,深入淺出地講解經典的五種 IO 模型。在此之前,先釐清幾個核心概念:
你點了最愛的蓋澆飯,然後原地站著,一直到飯做好端上桌才開吃。這就是典型的同步阻塞。廚師做飯時,你只能乾等。
網路編程中,預設調用 recvfrom 讀取客戶端數據,會一直阻塞至接收完成。這是最簡單的模型,適用於文件描述符(fd)較少、就緒迅速的場景。
你發現傻等不划算,點完餐回座位坐著,估摸著時間差不多了就去問老板:「飯好了沒?」好了就端,沒好繼續等,循環往複。
編程中對 socket 設置 O_NONBLOCK 即可實現。但注意:此方式僅對網路 IO 有效,磁碟 IO 無效。因為本地檔案除非被鎖定,否則不存在無限阻塞,故 O_NONBLOCK 僅在鎖場景下生效。此外,磁碟 IO 要么命中核心緩衝區直接返回,要么喚醒物理設備讀取,此時進程其餘工作均需等待,因此後續的 IO 多路復用和信號驅動對檔案 IO 也無意義。
補充:Nginx 和 Node.js 對本地檔案採用線程池模擬非阻塞;靜態檔案則利用 sendfile 等零拷貝技術極大提升效率。
點了好幾道菜,逐個去問顯得得不償失。不如讓老板(內核)幫忙:你預先告知關注哪些 fd,老板發現有菜(數據)備好就通知你。這就是 IO 多路復用,又稱多路 IO 就緒通知,讓單一進程能同時等待多個事件。
主流實現有 select、poll、epoll:
poll 通過 pollfd 陣列解決了 select 受 FD_SIZE 限制的問題。但兩者共同缺點是大量 fd 陣列在用戶態與核心態間整體拷貝,開銷隨 fd 數量線性增長。就像每次問老板,老板都得把你點的所有菜輪詢一遍再回覆,菜多且久備不齊時極其低效。epoll_wait 僅返回就緒的 fd。mmap 記憶體映射技術,避免內存拷貝開銷。目前 epoll 是 Linux 2.6+ 下最高效的 IO 多路復用方式,也是 Nginx、Node 的核心實現。FreeBSD 則有類似的 kqueue。
另需區分 水平觸發(LT) 與 邊緣觸發(ET):LT 下就緒 fd 未處理則下次查詢仍返回(select/poll 預設);ET 下無論是否處理,下次不再返回,理論性能更高但實現極其複雜,任何意外丟失事件都會導致請求處理錯誤。epoll 預設 LT,可通過選項啟用 ET。
你覺得每次去問太麻煩,對老板說:「哪道菜好了發短信通知我。」然後安心坐著或出門辦事。流程:開啟套接字信號驅動功能 → sigaction 註冊信號處理函數(非阻塞即時返回) → 數據就緒產生 SIGIO 信號 → 信號回調通知應用讀取數據。
你終於不想自己端菜了,要求老板:「飯做好直接端到我面前或送家裡。」這就是異步非阻塞。與信號驅動的關鍵區別:信號驅動由內核告知「何時可開始 IO 操作」(數據在核心緩衝區),而異步 IO 由內核通知「IO 操作已完成」(數據已在用戶空間)。
Unix 下 POSIX 1003.1 定義了 AIO 接口,使用 aio_read/aio_write 發起異步操作,aio_error 檢查狀態。
基於上述五種模型,Java 引入 NIO 和 NIO2.0 (AIO) 後,形成三種主流網路編程模型:
傳統模型:主線程 accept 阻塞 → 請求到達創建新線程處理 → 主線程繼續 accept。缺點明顯:客戶端連接增多導致線程暴增,系統性能急劇下降。改進方案如 Tomcat BIO Connector 採用線程池,有時稱為「偽異步 IO」。
JDK 1.4 引入,核心是 Selector 多路復用器,Linux 下基於 epoll 實現。流程:創建 ServerSocketChannel 綁定端口並設置非阻塞 → 創建 Reactor 線程與 Selector → 註冊 accept 事件 → Selector 無限循環輪詢就緒 Key → 處理新連接、讀取數據、業務邏輯。相比 BIO,編程複雜度大幅上升。
JDK 1.7 (NIO 2.0) 提供真正的異步非阻塞 IO,對應 Unix 異步模型。使用 AsynchronousServerSocketChannel、CompletionHandler 回調機制,編程模型較 NIO 顯著簡化。
| 模型 | 客戶端數:IO線程 | IO 模型 | 吞吐量 | 編程複雜度 |
|---|---|---|---|---|
| 同步阻塞 (BIO) | 1:1 | 同步阻塞 | 低 | 簡單 |
| 偽異步 (線程池) | M:N | 同步阻塞 | 中 | 簡單 |
| NIO | M:1 | 同步非阻塞 | 高 | 非常複雜 |
| AIO | M:0 | 異步非阻塞 | 高 | 複雜 |