摘要:的異步即是異步的,也是非阻塞的。但是,也可以進行一層稍微薄點的封裝,保留這種多路復用的模型,比如的,是一種同步非阻塞的模型。系統調用操作系統的系統調用提供了多路復用的非阻塞的系統調用,這也是機制實現需要用到的。
異步IO編程在javascript中得到了廣泛的應用,之前也寫過一篇博文進行梳理。
js的異步IO即是異步的,也是非阻塞的。非阻塞的IO需要底層操作系統的支持,比如在linux上的epoll系統調用。
從另外一個角度看待的話,底層操作系統對于非阻塞IO的系統調用是一種多路復用機制,js對其進行了比較厚的封裝,轉換成了異步IO。
但是,也可以進行一層稍微薄點的封裝,保留這種多路復用的模型,比如java的NIO,是一種同步非阻塞的IO模型。
非阻塞IO的一大優勢是,性能好,快??!這在對IO性能要求高的場景得到了大量應用,比如SOA框架。
傳統的同步IO方式,比如網絡傳輸,比如文件IO,在調用者調用read()時,調用會被一層一層調用下去直到OS的系統調用,調用者的線程會被阻塞。
當讀取完成時,該線程又會被喚醒,read()函數返回IO操作讀取的數據。
我們很容易能發現這種方式的特點及優劣:
接口容易理解,編程難度低。對調用者而言,read()就像一個普通的函數調用一樣,返回讀取的數據。只不過可能這個操作有點慢,這個函數執行時間長了一些而已。
在費時的IO操作時,線程需要等待IO完成。這意味著,如果你需要多個IO操作同時進行,就只能通過開多個線程來解決。
在客戶端編程時,第二點這個問題不大。客戶端程序對IO的并發要求不高,反而因為同步阻塞IO的接口易于編程而能夠減輕編程難度,代碼更直觀更可讀,從而變相的提高可調試性和開發效率。
服務端編程的特點然而,在服務器端編程的時候,這個劣勢就很明顯了,服務器端程序可能會面臨大量并發IO的考驗。
傳統的同步IO方式,比如說socket編程,服務器端的一個簡單的處理邏輯是這樣的:
使用一個線程監聽端口,如有客戶端的TCP連接連入,就交由處理線程處理。
每來一個TCP連接,就需要開一個線程來處理和該客戶端的邏輯。
在實際場景中會有很多優化技術,比如使用線程池。然而線程池僅僅是將TCP連接放入一個隊列里交由線程池中空閑的線程處理。
實質上,即使使用線程池,也改變不了正在被處理的每一個請求都需要占用一個多帶帶的線程這一事實。
這樣,會造成一些問題:
每一個請求需要一個線程來處理,但是服務器的線程數量是有上限的,這就限制了服務器的并發量。
線程本身的調度也占用一定的操作系統資源,在線程比較多的情況下,這個占用疊加起來就非常客觀。
多路復用IO 概念及模型java提供的NIO就是一種多路復用IO方式。
它能夠將多個IO操作用一個線程去管理,一個線程即可管理多個IO操作。
NIO的操作邏輯是這樣的,首先將需要監控的IO操作注冊到某個地方,并由一個線程管理。
當這些IO操作完成,會以事件的形式產生。該線程能夠獲取到完成的事件列表,并且對其進行處理。
java的NIO中有三個重要的概念:
Channel通道。表示一種IO原始源。如ServerSocketChannel表示監聽客戶端發起的TCP連接。
通過Channel能夠發起某種IO操作,但是卻立即返回不阻塞。
Buffer 緩沖區。Channel讀取或寫入的數據必須通過Buffer。網絡讀寫常用的是ByteBuffer。
Selector 選擇器。NIO中最核心的東西,將Channel注冊到Selector中,使得Selector能夠監控到該IO操作。
可以理解成Selecotr不斷輪詢被注冊的Channel,一旦Channel中有注冊的事件發生,便能處理發生的事件。
這里只是做個總結,看下下面的示例代碼就明白了。
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,首先是設定了它的監聽地址,這個與傳統的阻塞IO一致,給定一些初始的數據。傳統的阻塞IO之后會調用socket.accept()來獲取客戶端連接的TCP連接,這是一個阻塞的方法。
但是NIO在這里把ServerSocketChannel注冊到了Selector上,并且監控OP_ACCEPT事件。這個時候socket可以認為已經在監聽了,但是沒有阻塞線程。
之后,如果有TCP連接連接上,OP_ACCEPT事件就會產生,通過selector即可處理該事件。
因此,NIO的操作邏輯其實是事件驅動的。
后面的循環則是Selector處理的主邏輯。
第9行,這是一個阻塞的方法。它會等待被注冊的這些IO操作處理完成。一旦有一部分IO操作完成,它就會返回。
通過selector.selectedKeys()即可獲得完成的IO操作的事件。后面的代碼也就是在處理這些事件。
這部分完成的IO事件處理完畢后,就會循環的去處理下一批完成的IO事件,如此往復。
這里,我們可以清晰的看到,通過NIO的多路復用模型,我們通過一個線程,就能管理多個IO操作。
循環內部處理的邏輯,key.isAcceptable()可以認為是判斷該事件是否是OP_ACCEPT事件。是的話表示已經有客戶端TCP連接連接上了,第15行獲取該TCP連接的socket對象。由于是NIO編程,這是獲取到的是SocketChannel對象。
之后將該對象的OP_READ注冊到Selector上,發起IO讀操作,并且讓Selector監聽讀完成的事件。
后面的key.isReadable()也是同樣的道理,這里只有上面的代碼注冊了OP_READ事件,因此這里一定是上面的讀操作完成了產生的事件。
Buffer上面的代碼里,當有新的TCP連接連入時,調用回調函數onAccept;當對方傳輸數據給自己時,數據讀取完成后,調用回調函數onRead。
下面是這兩個回調函數的實現,它的功能很簡單:
當有TCP連接第一次連入時,發送hello 給對方。
當接收到對方傳來的數據時,原封不動的送回去。大概算是一個echo服務器。
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中的讀操作是非阻塞的。在之前數據的網絡傳輸已經完成了,這里只是處理傳輸完成的數據而已。
至于這里的寫操作是不是阻塞的。。。我覺得不是阻塞的,這一點我還不確定 ,時間有限,之后會經過代碼驗證,查更多資料去確認這一點。
所有的讀寫操作的數據都需要經過Buffer。那為什么要增加Buffer這一抽象概念?直接使用bytes[]不挺好嗎?
我猜測和NIO底層原理有關系,可能OS將數據傳輸到了操作系統原生的內存里,java使用的話復制到jvm內存中。我也不確定。。。 將來查更多資料去完善這一疑惑吧。
上面通過一個小DEMO,也就是一個簡單的ECHO服務器演示了NIO編程。下面來測試下結果:
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
效果不錯!不過這還沒完。
嘗試開啟多個終端,同時連接服務器,你會驚訝的發現,服務器能夠完美的同時和多個客戶端連接而不會出現“卡死”的情況。
回顧剛才的小DEMO我們可以發現,剛才的DEMO是 單線程 的,但是通過多路復用模型,卻能同時處理多個IO操作。
之前在博文《異步IO和同步IO》中也提到了一些異步IO的操作系統機制。
非阻塞IO需要操作系統機制的支持,在linux系統上,對應的是select/poll系統調用或epoll系統調用。
操作系統的作用之一是對硬件設備的管理,我們發現,負責運算的部件CPU和負責網絡傳輸的部件網卡,它們是互相獨立的,因此,它們實際上可以同時執行任務。那么,底層硬件的支持使得完全可以做到以下步驟:
CPU發送給網卡某些網絡IO操作請求,網卡接收到CPU接收到的請求。
網卡處理接收到的網絡IO操作任務,于此同時,CPU也能執行其它的計算工作。
當網卡的網絡IO操作完成后,通過硬件中斷機制給CPU發中斷。
CPU執行中斷處理程序,執行IO操作完成后的邏輯。
這里有個小小的問題,在讀取數據的時候,上面的步驟網卡讀取數據時顯然是不通過CPU的。以我個人有限的硬件知識推測,非阻塞IO的機制可能需要用到DMA。
仍然是個人推測,以后有時間去查閱相關資料去解決這個疑惑。
我們可以看到,硬件的運作方式天然就是異步的,也因此,操作系統也非常容易基于此進行抽象和封裝,向上提供非阻塞的IO系統調用。
OS系統調用linux操作系統的系統調用提供了多路復用的非阻塞IO的系統調用,這也是java NIO機制實現需要用到的。
在linux2.6之前,采用select/poll系統調用實現,而在linux2.6之后,采用epoll實現,使用紅黑樹優化過,也因此性能更高。
本篇博文梳理的java的NIO機制,這是一種多路復用模型,能夠使用一個線程去管理多個IO操作,避免傳統同步IO的線程開銷,大大提升性能。
從我個人的觀點,評判一種模型是否易用,一方面來看該模型是否與實際的問題特點相契合;另外一方面,看該模型需要開發者花多少成本在模型本身上而非業務邏輯上。
從這個標準出發,我們也不難發現,本身異步IO的回調方式就夠讓開發者頭疼的了,然而和異步IO相比,NIO比異步IO還要麻煩。
你需要花大量精力去時間去處理,去理解NIO本身的邏輯。因此,NIO的缺點是較高的開發成本和較晦澀的代碼,不優雅。
NIO在SOA框架,RPC框架等服務器領域有著較大的應用,除了java標準庫的NIO之外,這些實際生產的框架多使用第三方的NIO框架Netty。
原因之一是,java標準庫的NIO有一個bug,可能造成CPU 100%的占用。
今天,是我在公司實習呆的最后一天,我花了一個下午的時間去組織這篇博文。
感謝我的老大對我的器重和信任,給予我很多的機會去鍛煉,也給予了我很大的自由空間去研究技術,自我提升。
也感謝這段時間對我照顧,給予我幫助的同事們,祝福你們!
注:該文于2018-04-13撰寫于我的github靜態頁博客,現同步到我的segmentfault來。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71455.html
摘要:阻塞請求結果返回之前,當前線程被掛起。也就是說在異步中,不會對用戶線程產生任何阻塞。當前線程在拿到此次請求結果的過程中,可以做其它事情。事實上,可以只用一個線程處理所有的通道。 準備知識 同步、異步、阻塞、非阻塞 同步和異步說的是服務端消息的通知機制,阻塞和非阻塞說的是客戶端線程的狀態。已客戶端一次網絡請求為例做簡單說明: 同步同步是指一次請求沒有得到結果之前就不返回。 異步請求不會...
摘要:后改良為用線程池的方式代替新增線程,被稱為偽異步。最大的問題是阻塞,同步。每次請求都由程序執行并返回,這是同步的缺陷。這些都會被注冊在多路復用器上。多路復用器提供選擇已經就緒狀態任務的能力。并沒有采用的多路復用器,而是使用異步通道的概念。 Netty是一個提供異步事件驅動的網絡應用框架,用以快速開發高性能、高可靠的網絡服務器和客戶端程序。Netty簡化了網絡程序的開發,是很多框架和公司...
摘要:后改良為用線程池的方式代替新增線程,被稱為偽異步。最大的問題是阻塞,同步。每次請求都由程序執行并返回,這是同步的缺陷。這些都會被注冊在多路復用器上。多路復用器提供選擇已經就緒狀態任務的能力。并沒有采用的多路復用器,而是使用異步通道的概念。 Netty是一個提供異步事件驅動的網絡應用框架,用以快速開發高性能、高可靠的網絡服務器和客戶端程序。Netty簡化了網絡程序的開發,是很多框架和公司...
摘要:該線程在此期間不能再干任何事情了。線程通訊線程之間通過等方式通訊。選擇器傳統的模式會基于服務器會為每個客戶端請求建立一個線程由該線程單獨負貴處理一個客戶請求。 本文是對NIO知識的歸納與整理 1.阻塞與同步 1)阻塞(Block)和非租塞(NonBlock): 阻塞和非阻塞是進程在訪問數據的時候,數據是否準備就緒的一種處理方式,當數據沒有準備的時候阻塞:往往需要等待缞沖區中的數據準備好...
摘要:最近面試問的比較多的問題就是這一塊了,有些也答出來了,有些答的不好,最近這段時間開始深入了解一些這方面的東西,也想總結一下。 最近面試問的比較多的問題就是IO這一塊了,有些也答出來了,有些答的不好,最近這段時間開始深入了解一些這方面的東西,也想總結一下。 前置點 1,用戶空間系統空間 Linux系統會把一個進程分為兩個空間,用戶空間和系統空間,比如我們正常的編碼,操作的都是用戶空...
閱讀 2491·2021-11-25 09:43
閱讀 2585·2021-11-16 11:50
閱讀 3280·2021-10-09 09:44
閱讀 3193·2021-09-26 09:55
閱讀 2834·2019-08-30 13:50
閱讀 1026·2019-08-29 13:24
閱讀 2068·2019-08-26 11:44
閱讀 2790·2019-08-26 11:37