它如何解決IO等待問題?
Go有超高并發能力,它如何解決IO等待問題的?回答此問題前,我們需要先普及一下 IO 的相關知識點。
01IO 基本概念Linux 的內核將所有外部設備都可以看做一個文件來操作(Unix 的設計原則,一切皆文件)。那么我們對外部設備的操作都可以看做對文件進行操作。我們對一個文件的讀寫,都通過調用內核提供的系統調用;內核給我們返回一個 file descriptor(fd,文件描述符)。對一個 socket 的讀寫也會有相應的描述符,稱為socketfd(socket 描述符)。描述符就是一個數字(可以理解為一個索引),指向內核中一個結構體(文件路徑,數據區,等一些屬性)。應用程序對文件的讀寫就通過對描述符的讀寫完成。一個基本的 IO,它會涉及到兩個系統對象,一個是調用這個 IO 的進程對象,另一個就是系統內核(kernel)。一般來說,服務器端的 I/O 主要有兩種情況:一是來自網絡的 I/O;二是對文件(設備)的I/O。02常見的 IO 模型首先一個 IO 操作其實分成了兩個步驟:發起 IO 請求(等待網絡數據到達網卡并讀取到內核緩沖區,數據準備好)和實際的 IO 操作(從內核緩沖區復制數據到進程空間)。阻塞和非阻塞阻塞 IO 和非阻塞 IO 的區別在于第一步:發起 IO 請求是否會被阻塞。如果阻塞直到完成,那么就是傳統的阻塞 IO,如果不阻塞,那么就是非阻塞 IO。同步和異步同步 IO 和異步 IO 的區別就在于第二個步驟是否阻塞。如果實際的 IO 讀寫阻塞請求進程,那么就是同步IO。因此常說的阻塞 IO、非阻塞 IO、IO 復用、信號驅動 IO 都是同步 IO。如果不阻塞,而是操作系統幫你做完 IO 操作后再將結果返回給你(通知你),那么就是異步IO。IO 多路復用指內核一旦發現進程指定的一個或者多個IO條件準備讀取,它就通知該進程。目前支持 I/O 多路復用的常用系統調用有 select,pselect,poll,epoll 等,I/O 多路復用就是通過一種機制,一個進程可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但 select,pselect,poll,epoll本質上都是同步 I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步 I/O 則無需自己負責進行讀寫,異步 I/O 的實現會負責把數據從內核拷貝到用戶空間。5 種 IO 模型
《UNIX 網絡編程》對 IO 模型進行了總結,分別是:
阻塞 IO、非阻塞 IO、IO 多路復用、信號驅動的 IO、異步 IO;前 4 種為同步 IO,只有異步 IO 模型是異步 IO。03回到問題目前很多高性能的基礎網絡服務器都是采用的 C 語言開發的,比如:Nginx、Redis、memcached 等,它們都是基于“事件驅動 + 事件回調函數”的方式實現,也就是采用 epoll 等作為網絡收發數據包的核心驅動。但不少人都認為“事件驅動 + 事件回調函數”的編程方法是“反人類”的;因為大多數人都更習慣線性的處理一件事情:做完第一件事情再做第二件事情,并不習慣在 N 件事情之間頻繁的切換干活。為了解決程序員在開發服務器時需要自己的大腦不斷的“上下文切換”的問題,Go 語言引入了一種用戶態線程 goroutine 來取代編寫異步的事件回調函數,從而重新回歸到多線程并發模型的線性、同步的編程方式上。在 Linux 上 Go 語言寫的網絡服務器也是采用的 epoll 作為最底層的數據收發驅動,Go 語言網絡的底層實現中同樣存在“上下文切換”的工作,只是這個切換工作由 runtime 的調度器來做了,減少了程序員的負擔。所以,IO 等待是必然,只是誰等的問題。Go 語言在遇到 IO 需要等待時,runtime 會進行調度,語言層面處理這個問題。Go 擁有超高并發能力的關鍵就在于用戶態的 goroutine。
Go語言中文網,致力于每日分享編碼知識,歡迎關注我,會有意想不到的收獲!