摘要:的異步即是異步的,也是非阻塞的。但是,也可以進行一層稍微薄點的封裝,保留這種多路復(fù)用的模型,比如的,是一種同步非阻塞的模型。系統(tǒng)調(diào)用操作系統(tǒng)的系統(tǒng)調(diào)用提供了多路復(fù)用的非阻塞的系統(tǒng)調(diào)用,這也是機制實現(xiàn)需要用到的。
異步IO編程在javascript中得到了廣泛的應(yīng)用,之前也寫過一篇博文進行梳理。
js的異步IO即是異步的,也是非阻塞的。非阻塞的IO需要底層操作系統(tǒng)的支持,比如在linux上的epoll系統(tǒng)調(diào)用。
從另外一個角度看待的話,底層操作系統(tǒng)對于非阻塞IO的系統(tǒng)調(diào)用是一種多路復(fù)用機制,js對其進行了比較厚的封裝,轉(zhuǎn)換成了異步IO。
但是,也可以進行一層稍微薄點的封裝,保留這種多路復(fù)用的模型,比如java的NIO,是一種同步非阻塞的IO模型。
非阻塞IO的一大優(yōu)勢是,性能好,快??!這在對IO性能要求高的場景得到了大量應(yīng)用,比如SOA框架。
傳統(tǒng)的同步IO方式,比如網(wǎng)絡(luò)傳輸,比如文件IO,在調(diào)用者調(diào)用read()時,調(diào)用會被一層一層調(diào)用下去直到OS的系統(tǒng)調(diào)用,調(diào)用者的線程會被阻塞。
當讀取完成時,該線程又會被喚醒,read()函數(shù)返回IO操作讀取的數(shù)據(jù)。
我們很容易能發(fā)現(xiàn)這種方式的特點及優(yōu)劣:
接口容易理解,編程難度低。對調(diào)用者而言,read()就像一個普通的函數(shù)調(diào)用一樣,返回讀取的數(shù)據(jù)。只不過可能這個操作有點慢,這個函數(shù)執(zhí)行時間長了一些而已。
在費時的IO操作時,線程需要等待IO完成。這意味著,如果你需要多個IO操作同時進行,就只能通過開多個線程來解決。
在客戶端編程時,第二點這個問題不大??蛻舳顺绦?qū)O的并發(fā)要求不高,反而因為同步阻塞IO的接口易于編程而能夠減輕編程難度,代碼更直觀更可讀,從而變相的提高可調(diào)試性和開發(fā)效率。
服務(wù)端編程的特點然而,在服務(wù)器端編程的時候,這個劣勢就很明顯了,服務(wù)器端程序可能會面臨大量并發(fā)IO的考驗。
傳統(tǒng)的同步IO方式,比如說socket編程,服務(wù)器端的一個簡單的處理邏輯是這樣的:
使用一個線程監(jiān)聽端口,如有客戶端的TCP連接連入,就交由處理線程處理。
每來一個TCP連接,就需要開一個線程來處理和該客戶端的邏輯。
在實際場景中會有很多優(yōu)化技術(shù),比如使用線程池。然而線程池僅僅是將TCP連接放入一個隊列里交由線程池中空閑的線程處理。
實質(zhì)上,即使使用線程池,也改變不了正在被處理的每一個請求都需要占用一個多帶帶的線程這一事實。
這樣,會造成一些問題:
每一個請求需要一個線程來處理,但是服務(wù)器的線程數(shù)量是有上限的,這就限制了服務(wù)器的并發(fā)量。
線程本身的調(diào)度也占用一定的操作系統(tǒng)資源,在線程比較多的情況下,這個占用疊加起來就非??陀^。
多路復(fù)用IO 概念及模型java提供的NIO就是一種多路復(fù)用IO方式。
它能夠?qū)⒍鄠€IO操作用一個線程去管理,一個線程即可管理多個IO操作。
NIO的操作邏輯是這樣的,首先將需要監(jiān)控的IO操作注冊到某個地方,并由一個線程管理。
當這些IO操作完成,會以事件的形式產(chǎn)生。該線程能夠獲取到完成的事件列表,并且對其進行處理。
java的NIO中有三個重要的概念:
Channel通道。表示一種IO原始源。如ServerSocketChannel表示監(jiān)聽客戶端發(fā)起的TCP連接。
通過Channel能夠發(fā)起某種IO操作,但是卻立即返回不阻塞。
Buffer 緩沖區(qū)。Channel讀取或?qū)懭氲臄?shù)據(jù)必須通過Buffer。網(wǎng)絡(luò)讀寫常用的是ByteBuffer。
Selector 選擇器。NIO中最核心的東西,將Channel注冊到Selector中,使得Selector能夠監(jiān)控到該IO操作。
可以理解成Selecotr不斷輪詢被注冊的Channel,一旦Channel中有注冊的事件發(fā)生,便能處理發(fā)生的事件。
這里只是做個總結(jié),看下下面的示例代碼就明白了。
Selector和Channelprivate void exec(int port) throws IOException { Selector selector = Selector.open(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.socket().bind(new InetSocketAddress(port)); serverSocketChannel.configureBlocking(false); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { int n = selector.select(); // Block Iteratorit = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); if (key.isAcceptable()) { ServerSocketChannel server = (ServerSocketChannel) key.channel(); SocketChannel channel = server.accept(); if (channel != null) { channel.configureBlocking(false); channel.register(selector, SelectionKey.OP_READ); onAccept(channel); } } if (key.isReadable()) { SocketChannel socketChannel = (SocketChannel) key.channel(); onRead(socketChannel); } it.remove(); } } }
來一步一步的分析這些代碼。
首先,第3行到第6行是對通道ServerSocketChannel的操作。
對于這個ServerSocketChannel,首先是設(shè)定了它的監(jiān)聽地址,這個與傳統(tǒng)的阻塞IO一致,給定一些初始的數(shù)據(jù)。傳統(tǒng)的阻塞IO之后會調(diào)用socket.accept()來獲取客戶端連接的TCP連接,這是一個阻塞的方法。
但是NIO在這里把ServerSocketChannel注冊到了Selector上,并且監(jiān)控OP_ACCEPT事件。這個時候socket可以認為已經(jīng)在監(jiān)聽了,但是沒有阻塞線程。
之后,如果有TCP連接連接上,OP_ACCEPT事件就會產(chǎn)生,通過selector即可處理該事件。
因此,NIO的操作邏輯其實是事件驅(qū)動的。
后面的循環(huán)則是Selector處理的主邏輯。
第9行,這是一個阻塞的方法。它會等待被注冊的這些IO操作處理完成。一旦有一部分IO操作完成,它就會返回。
通過selector.selectedKeys()即可獲得完成的IO操作的事件。后面的代碼也就是在處理這些事件。
這部分完成的IO事件處理完畢后,就會循環(huán)的去處理下一批完成的IO事件,如此往復(fù)。
這里,我們可以清晰的看到,通過NIO的多路復(fù)用模型,我們通過一個線程,就能管理多個IO操作。
循環(huán)內(nèi)部處理的邏輯,key.isAcceptable()可以認為是判斷該事件是否是OP_ACCEPT事件。是的話表示已經(jīng)有客戶端TCP連接連接上了,第15行獲取該TCP連接的socket對象。由于是NIO編程,這是獲取到的是SocketChannel對象。
之后將該對象的OP_READ注冊到Selector上,發(fā)起IO讀操作,并且讓Selector監(jiān)聽讀完成的事件。
后面的key.isReadable()也是同樣的道理,這里只有上面的代碼注冊了OP_READ事件,因此這里一定是上面的讀操作完成了產(chǎn)生的事件。
Buffer上面的代碼里,當有新的TCP連接連入時,調(diào)用回調(diào)函數(shù)onAccept;當對方傳輸數(shù)據(jù)給自己時,數(shù)據(jù)讀取完成后,調(diào)用回調(diào)函數(shù)onRead。
下面是這兩個回調(diào)函數(shù)的實現(xiàn),它的功能很簡單:
當有TCP連接第一次連入時,發(fā)送hello 給對方。
當接收到對方傳來的數(shù)據(jù)時,原封不動的送回去。大概算是一個echo服務(wù)器。
private void onRead(SocketChannel socketChannel) throws IOException { ByteBuffer buffer = ByteBuffer.allocateDirect(1024); int count; while ((count = socketChannel.read(buffer)) > 0) { buffer.flip(); while (buffer.hasRemaining()) { socketChannel.write(buffer); } buffer.clear(); } if (count < 0) { socketChannel.close(); } } private void onAccept(SocketChannel channel) throws IOException { System.out.println(channel.socket().getInetAddress() + "/" + channel.socket().getPort()); ByteBuffer buffer = ByteBuffer.allocateDirect(1024); buffer.put("hello ".getBytes()); buffer.flip(); channel.write(buffer); }
從上面的代碼可以看出:
onRead中的讀操作是非阻塞的。在之前數(shù)據(jù)的網(wǎng)絡(luò)傳輸已經(jīng)完成了,這里只是處理傳輸完成的數(shù)據(jù)而已。
至于這里的寫操作是不是阻塞的。。。我覺得不是阻塞的,這一點我還不確定 ,時間有限,之后會經(jīng)過代碼驗證,查更多資料去確認這一點。
所有的讀寫操作的數(shù)據(jù)都需要經(jīng)過Buffer。那為什么要增加Buffer這一抽象概念?直接使用bytes[]不挺好嗎?
我猜測和NIO底層原理有關(guān)系,可能OS將數(shù)據(jù)傳輸?shù)搅瞬僮飨到y(tǒng)原生的內(nèi)存里,java使用的話復(fù)制到j(luò)vm內(nèi)存中。我也不確定。。。 將來查更多資料去完善這一疑惑吧。
上面通過一個小DEMO,也就是一個簡單的ECHO服務(wù)器演示了NIO編程。下面來測試下結(jié)果:
frapples:~ ?> nc -nvv 127.0.0.1 4040 Connection to 127.0.0.1 4040 port [tcp/*] succeeded! hello jfldjfl jfldjfl jfldjflieu jfldjflieu jfldhgldjfljdl jfldhgldjfljdl
效果不錯!不過這還沒完。
嘗試開啟多個終端,同時連接服務(wù)器,你會驚訝的發(fā)現(xiàn),服務(wù)器能夠完美的同時和多個客戶端連接而不會出現(xiàn)“卡死”的情況。
回顧剛才的小DEMO我們可以發(fā)現(xiàn),剛才的DEMO是 單線程 的,但是通過多路復(fù)用模型,卻能同時處理多個IO操作。
之前在博文《異步IO和同步IO》中也提到了一些異步IO的操作系統(tǒng)機制。
非阻塞IO需要操作系統(tǒng)機制的支持,在linux系統(tǒng)上,對應(yīng)的是select/poll系統(tǒng)調(diào)用或epoll系統(tǒng)調(diào)用。
操作系統(tǒng)的作用之一是對硬件設(shè)備的管理,我們發(fā)現(xiàn),負責(zé)運算的部件CPU和負責(zé)網(wǎng)絡(luò)傳輸?shù)牟考W(wǎng)卡,它們是互相獨立的,因此,它們實際上可以同時執(zhí)行任務(wù)。那么,底層硬件的支持使得完全可以做到以下步驟:
CPU發(fā)送給網(wǎng)卡某些網(wǎng)絡(luò)IO操作請求,網(wǎng)卡接收到CPU接收到的請求。
網(wǎng)卡處理接收到的網(wǎng)絡(luò)IO操作任務(wù),于此同時,CPU也能執(zhí)行其它的計算工作。
當網(wǎng)卡的網(wǎng)絡(luò)IO操作完成后,通過硬件中斷機制給CPU發(fā)中斷。
CPU執(zhí)行中斷處理程序,執(zhí)行IO操作完成后的邏輯。
這里有個小小的問題,在讀取數(shù)據(jù)的時候,上面的步驟網(wǎng)卡讀取數(shù)據(jù)時顯然是不通過CPU的。以我個人有限的硬件知識推測,非阻塞IO的機制可能需要用到DMA。
仍然是個人推測,以后有時間去查閱相關(guān)資料去解決這個疑惑。
我們可以看到,硬件的運作方式天然就是異步的,也因此,操作系統(tǒng)也非常容易基于此進行抽象和封裝,向上提供非阻塞的IO系統(tǒng)調(diào)用。
OS系統(tǒng)調(diào)用linux操作系統(tǒng)的系統(tǒng)調(diào)用提供了多路復(fù)用的非阻塞IO的系統(tǒng)調(diào)用,這也是java NIO機制實現(xiàn)需要用到的。
在linux2.6之前,采用select/poll系統(tǒng)調(diào)用實現(xiàn),而在linux2.6之后,采用epoll實現(xiàn),使用紅黑樹優(yōu)化過,也因此性能更高。
本篇博文梳理的java的NIO機制,這是一種多路復(fù)用模型,能夠使用一個線程去管理多個IO操作,避免傳統(tǒng)同步IO的線程開銷,大大提升性能。
從我個人的觀點,評判一種模型是否易用,一方面來看該模型是否與實際的問題特點相契合;另外一方面,看該模型需要開發(fā)者花多少成本在模型本身上而非業(yè)務(wù)邏輯上。
從這個標準出發(fā),我們也不難發(fā)現(xiàn),本身異步IO的回調(diào)方式就夠讓開發(fā)者頭疼的了,然而和異步IO相比,NIO比異步IO還要麻煩。
你需要花大量精力去時間去處理,去理解NIO本身的邏輯。因此,NIO的缺點是較高的開發(fā)成本和較晦澀的代碼,不優(yōu)雅。
NIO在SOA框架,RPC框架等服務(wù)器領(lǐng)域有著較大的應(yīng)用,除了java標準庫的NIO之外,這些實際生產(chǎn)的框架多使用第三方的NIO框架Netty。
原因之一是,java標準庫的NIO有一個bug,可能造成CPU 100%的占用。
今天,是我在公司實習(xí)呆的最后一天,我花了一個下午的時間去組織這篇博文。
感謝我的老大對我的器重和信任,給予我很多的機會去鍛煉,也給予了我很大的自由空間去研究技術(shù),自我提升。
也感謝這段時間對我照顧,給予我?guī)椭耐聜?,祝福你們?/p>
注:該文于2018-04-13撰寫于我的github靜態(tài)頁博客,現(xiàn)同步到我的segmentfault來。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/71455.html
摘要:阻塞請求結(jié)果返回之前,當前線程被掛起。也就是說在異步中,不會對用戶線程產(chǎn)生任何阻塞。當前線程在拿到此次請求結(jié)果的過程中,可以做其它事情。事實上,可以只用一個線程處理所有的通道。 準備知識 同步、異步、阻塞、非阻塞 同步和異步說的是服務(wù)端消息的通知機制,阻塞和非阻塞說的是客戶端線程的狀態(tài)。已客戶端一次網(wǎng)絡(luò)請求為例做簡單說明: 同步同步是指一次請求沒有得到結(jié)果之前就不返回。 異步請求不會...
摘要:后改良為用線程池的方式代替新增線程,被稱為偽異步。最大的問題是阻塞,同步。每次請求都由程序執(zhí)行并返回,這是同步的缺陷。這些都會被注冊在多路復(fù)用器上。多路復(fù)用器提供選擇已經(jīng)就緒狀態(tài)任務(wù)的能力。并沒有采用的多路復(fù)用器,而是使用異步通道的概念。 Netty是一個提供異步事件驅(qū)動的網(wǎng)絡(luò)應(yīng)用框架,用以快速開發(fā)高性能、高可靠的網(wǎng)絡(luò)服務(wù)器和客戶端程序。Netty簡化了網(wǎng)絡(luò)程序的開發(fā),是很多框架和公司...
摘要:后改良為用線程池的方式代替新增線程,被稱為偽異步。最大的問題是阻塞,同步。每次請求都由程序執(zhí)行并返回,這是同步的缺陷。這些都會被注冊在多路復(fù)用器上。多路復(fù)用器提供選擇已經(jīng)就緒狀態(tài)任務(wù)的能力。并沒有采用的多路復(fù)用器,而是使用異步通道的概念。 Netty是一個提供異步事件驅(qū)動的網(wǎng)絡(luò)應(yīng)用框架,用以快速開發(fā)高性能、高可靠的網(wǎng)絡(luò)服務(wù)器和客戶端程序。Netty簡化了網(wǎng)絡(luò)程序的開發(fā),是很多框架和公司...
摘要:該線程在此期間不能再干任何事情了。線程通訊線程之間通過等方式通訊。選擇器傳統(tǒng)的模式會基于服務(wù)器會為每個客戶端請求建立一個線程由該線程單獨負貴處理一個客戶請求。 本文是對NIO知識的歸納與整理 1.阻塞與同步 1)阻塞(Block)和非租塞(NonBlock): 阻塞和非阻塞是進程在訪問數(shù)據(jù)的時候,數(shù)據(jù)是否準備就緒的一種處理方式,當數(shù)據(jù)沒有準備的時候阻塞:往往需要等待缞沖區(qū)中的數(shù)據(jù)準備好...
摘要:最近面試問的比較多的問題就是這一塊了,有些也答出來了,有些答的不好,最近這段時間開始深入了解一些這方面的東西,也想總結(jié)一下。 最近面試問的比較多的問題就是IO這一塊了,有些也答出來了,有些答的不好,最近這段時間開始深入了解一些這方面的東西,也想總結(jié)一下。 前置點 1,用戶空間系統(tǒng)空間 Linux系統(tǒng)會把一個進程分為兩個空間,用戶空間和系統(tǒng)空間,比如我們正常的編碼,操作的都是用戶空...
閱讀 2506·2021-11-25 09:43
閱讀 2617·2021-11-16 11:50
閱讀 3300·2021-10-09 09:44
閱讀 3207·2021-09-26 09:55
閱讀 2847·2019-08-30 13:50
閱讀 1034·2019-08-29 13:24
閱讀 2095·2019-08-26 11:44
閱讀 2806·2019-08-26 11:37