[TOC] 前言 這篇文章主要是總結自己對於網路編程中非同步,同步,阻塞和非阻塞的理解,這個問題自從學習NIO以來一直困擾著我,,其實想來很久就想寫了,只不過當時理解不夠,無從下手。最近在學習VertX框架,又去熟悉了下Netty的代碼,因為了對於多線程也有了更深的理解,所以才開始對於這些概念有了理解 ...
目錄
前言
這篇文章主要是總結自己對於網路編程中非同步,同步,阻塞和非阻塞的理解,這個問題自從學習NIO以來一直困擾著我,,其實想來很久就想寫了,只不過當時理解不夠,無從下手。最近在學習VertX框架,又去熟悉了下Netty的代碼,因為了對於多線程也有了更深的理解,所以才開始對於這些概念有了理解,用於理清思路,本文需要有良好的多線程和網路編程基礎,不適合初學者。
一、非同步,同步,阻塞和非阻塞的理解
關於這四個概念在IO方面的理解我貼兩個鏈接,他們已經有了很好的說明我就不再講述:
以前在學習c++中muduo只是記得陳碩說的epoll是一個同步非阻塞的模型,但是網上很多人說Reactor模型是一個非同步阻塞的模型,在學習Netty的時候官網是這麼介紹的:
Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.
Netty是一個非同步的高性能網路框架,那麼到底是誰說錯了?
其實大家都沒有錯誤,只是角度不同。
先說說什麼IO是非同步的?非同步其實是針對數據從內核拷貝到用戶進程空間這個操作是誰完成的,同步IO非常好理解,當用戶進程發起一個read操作的時候發生一次系統調用,然後內核檢查有沒有數據,如果有則複製數據到進程空間,用戶進程繼續執行。而非同步IO中複製數據到進程空間這個操作是內核幫你完成的,等完成之後再來通知你,執行你的邏輯。Reactor模型中,EventLoop線程在select到有可讀數據之後,然後在自己去讀取數據,所以從這個角度來講Reactor模型確實是同步的,在Linux的五種IO模型中只有非同步IO是非同步的。
那麼為什麼Netty說他是一個非同步網路庫呢,這其實是另一個角度的闡述,對於網路庫的作者來說,他們面向的是Linux提供的這些api,所以說多路復用的Reactor是同步的沒問題。那麼對於Netty的使用者來說,我們面向的是Netty,Netty進一步封裝了IO操作,在我們發起IO操作的時候它返回了一個Future,我們可以提供一個監聽器來傳入我們的回調,當IO操作完成時會執行我們的邏輯,我們的這個操作相對於Netty就是非同步的。
所以Reactor是同步非阻塞的,Netty是非同步非阻塞的。
二、非同步編程從用戶層面和框架層面不同角度的理解
Java中的Future是非同步的嗎?
對於這個問題,我想相信很多同學都會認為是非同步的,這裡我認為是同步的,下麵談談我的理解。
先想想一個非同步操作需要哪些元素,我認為需要發起者,執行者,執行邏輯,回調邏輯。流程: 發起者請求執行者去執行所需邏輯,然後在成功之後調用回調邏輯。Future中缺了什麼?沒錯,就是那個回調!
我們使用Future的模式一般是:投遞一個任務到線程池得有個Future,然後去執行其他可以並行的操作,操作完之後去調用Future的get方法獲取結果或者isDone判斷是否執行完畢。這裡的Future只是對於計算結果的一個建模,我們在後面需要使用的時候再去輪詢(輪詢也是同步非阻塞的一個標誌)或者阻塞,他提供的了一個非常好的特性:非阻塞!所以我認為Future是一個同步非阻塞的實現。也正是因為Future沒有實現非同步的特性,在jdk1.8之後新增了CompletableFuture提供了非同步的特性。
註意非同步元素的發起者和執行者可以是同一個線程,最常見的例子就是NodeJs的單線程模型。拿Netty的線程來具體,你在EventLoop中發起一個寫請求後得到一個Future,你可以設置回調,下次執行這個回調的還是EventLoop線程
用戶角度的理解
這裡主要說說在使用非同步編程的一點理解,因為平時還是用為主,我們作為框架的使用者有必要瞭解一些常見的使用範式。就我目前接觸的最多還是CompletableFuture,Netty和VertX,當時也寫過一點Js,Js主要也是回調的用法。我知道的用法如下:
- 回調 這種是最常見的,相信也是最容易理解的,Js和VertX很多都採用了這個實現,我們在調用一個函數的時候提供一個響應結果的回調。響應式編程就是結合函數式和非同步回調的一個產物,我相信以後會越來越常見
- 監聽器 這個是Netty的實現,Netty將很多同步的地方改成了非同步同時返回一個Future,我們可以通過這個Future添加監聽器,執行得到結果時的邏輯
- 組合式 相對於回調式,在實現多個回調時代碼扁平化,可以瞭解下CompletableFuture的用法和實現真的是非常的優雅
因為非同步的高性能,很多時候我們自己也想把一個操作封裝成非同步的,就需要明白到底什麼是非同步,明白非同步需要的元素,你會發現如果不藉助以後的非同步組件將一個操作封裝成非同步非常的困難,所以最簡單的方案就是將你的回調最終傳遞到已有非同步的組件中。
舉2個簡單的例子:
- 我們利用
CompletableFuture.supplyAsync(Object::new).thenAccept(o -> System.out.println(o));
這一行非常簡單的代碼實現了一個非同步,Object::new
會被投遞到線程池中,然後執行完成後執行列印語句。 - VertX的例子,VertX將很多同步的操作封裝成了非同步的操作,比如場景的發起Http請求的,他的底層實現就是將這個操作委托給了Netty
框架角度的理解
框架層面的理解有助於我們在寫代碼中不會用錯。有沒有想過一個非同步操作框架給你做了什麼?
當你發起一個操作的時候,框架會去執行你的邏輯,在執行完畢時(成功或異常)去修改狀態並執行你的回調。修改狀態並執行你的回調這個操作在JDK中放在了CompletableFuture中,在Netty中則單獨採用了Promise介面,其實兩者的實現是非常類似的(方法名都取的差不多)。以Netty舉例分為Future和Promise兩個方法,作為用戶我們更應該關心Future的介面,Promise是框架層面需要實現的,我們在自己去實現的時候值得我們去學習裡面的思想。
不過我認為我們直接使用Promise的這種介面的機會很少,Netty和VertX場景下還是有機會用到,在用到Promise介面的時候應該考慮下是否合理,檢查下是不是在同一個線程中,是不是可以簡單的介面代替。給一個簡單的錯誤示例:
這裡說下Promise,我們知道Js中也有一個Promise,千萬不要當成類似的東西,兩者毫無干系,Netty的Promise是對完成操作的行為的建模,Js的Promise是為了組合各個非同步的調用。
import io.VertX.core.Future;
public class AuctionHandler {
public Future<Void> handle() {
// 請求級別變數
Context context = new Context();
context.future.tryComplete();
return context.future;
}
public static class Context {
Future<Void> future = Future.future();
}
public static void main(String[] args) {
// 註意這裡的handle方法返回的Future是VertX的。
// 這裡的方法都是在同一個線程中執行的,完全沒有非同步化,所以可以改成傳遞一個普通的介面即可
new AuctionHandler().handle().setHandler(event1 -> System.out.println("handler exec!"));
}
}
雖然這個的代碼錯誤看上去很低級,但是在開發VertX應用時需要時刻保持警惕。另外還有一點需要說明:當返回給你的Future已經是完成狀態時,如上面的代碼示例,你再增加回調,這個回調還會被執行,Netty和CompletableFuture在添加回調的時候都是檢查狀態是否完成,完成的話直接投遞到相應線程執行。
三、為什麼使用非同步
為什麼要使用非同步,相信很多同學都知道是為了高性能,那麼非同步為什麼高性能?
這裡先談談NodeJs和Java,對於NodeJs,很多人聽說性能十分高,"秒殺"Java。我當時一直無法理解,為什麼Js能超過Java,
首先Node是單線程的,雖然可以藉助第三方庫來實現多線程,另外Jvm作為業界最優秀的虛擬機,那麼Node到底是靠了什麼超過了Java?這裡的關鍵就在於Node的Io模型採用了Reactor模型,可以處理大量的連接。Java中的Web開發是以Servlet為主導,採用了同步阻塞模型,雖然用線程池實現n個連接用m和線程做優化,但是當有大量連接時,線程數量過多導致的線程調度成本會很高,另外線上程處理Io的時候也是同步阻塞,如果對方返回很難會導致當前線程一直無法釋放,所以Tomcat這種不適合處理大量連接的場景。
我們知道Jetty的底層實現就是Reactor模型,Tomcat在8之後預設也用了Reactor是不是會大幅提高性能?不幸的是,雖然可以提高一些性能但是還是無法和Node一較高低,他解決的是Http連接那一塊的阻塞問題,但是由於Servlet的編程模型,大量的同步阻塞操作還是無法避免,比如你在一個請求中去訪問了資料庫,這個線程就會一直被占用,一定程度上你可以通過增加線程來緩解但是線程過多又會增加調度的成本,可能會導致虛擬機假死。所以如果你的處理中有這種耗時操作,那他就是你的瓶頸,你的qps的上限就很低。在高併發場景下,Servlet的瓶頸會十分突出,只能通過大量的堆機器來水平擴展,但是沒有很好的榨乾伺服器的性能。
所以我們需要的是編程模型的改變,像Nodejs那樣在同步阻塞的地方進行非同步非阻塞或者非同步阻塞化。Spring5.0中的 WebFlux給了一個對應的解決方案,提供了響應式編程的模型用以取代Servlet,他對常見同步阻塞的地方進行了重寫,如Redis和Mysql等常見的IO。很早之前VertX(早期名字Node.X,Java版的Nodejs)框架也提供了這樣的編程模型,對很多同步阻塞的地方進行了重寫,這個框架十分輕量級,社區活躍度非常高,使用起來非常方便。這兩個底層都是Netty,不得不說Netty實在是太強大了。也從另外一個角度說明設計的重要性,語言反而是其次。NodeJs,WebFlux和VertX都採用了類似的Reactor模型,高性能伺服器領域這個模型幾乎已經是最佳實踐,理解這個模型就和多線程一樣重要。我覺得拿Servlet和NodeJs來做性能的對比,是十分不公平的。NodeJs在Java領悟的對手應該是VertX這種框架,關於高性能Web框架的對比,techempower這個網站已經給出了詳細的排名,排名前十的大部分是Jvm語言,Nodejs在五十名之後了,所以不要在拿Servlet去和NodeJs做對比了,Servlet這種模型在高併發領悟一定會被逐漸取代。所以要深入理解響應式編程,擁抱響應式編程,現有的代碼以及未來的開發都可以用響應式編程來做優化。
那麼非同步到底解決了什麼問題?
上面舉的例子只是簡單說明瞭現有的非同步非阻塞框架的性能優勢。但是這個問題我也無法給出準確的解釋,只是談談我自己的理解:
- 非阻塞很好理解,如果是阻塞的,那個當前的用戶線程一定被hang住,直到數據寫完或者讀完(這個過程中這個線程就是沒用的,所以我們需要開啟大量的線程),如果非阻塞可以立即返回,繼續處理其他任務。
- 非同步的理解我用一個例子來說名:Netty中發起一個寫操作時立即返回了一個Future,用戶可以提供一個監聽器執行寫操作完成後的邏輯。試想如果這裡是同步非阻塞的,即調用Future的sync方法(不要在EventLoop中調用,導致死鎖),那就會白白浪費一個線程,
程式運行過程中始終是圍繞著兩個主題:IO、CPU。CPU和IO的速度差距十分大,非同步和Reactor模型都是為了平衡這個差距,讓CPU能充分利用起來,不要因為IO和其他同步操作導致線程Hang住,始終處於可運行的狀態,可以使用少量的線程充分利用CPU。
四、理解這些能在實際中的應用
很多人可能會疑問就算了把這些弄的明明白白到底有什麼用?其實如果你很好的掌握了Reactor的編程模型,很多問題就能想明白了下麵談下自己理解的有用的地方:
- 如果用過Redis都瞭解他是單線程來處理用戶的請求的,他實際就是採用了Reactor模型來處理請求,也就很好的理解了為什麼Redis單線程能保持很好的性能。知道了他的實現,在使用的過程中就知道儘量避免大對象的傳輸,因為是單線程處理,如果一個連接傳輸大對象那麼別的連接的請求將不能被及時處理。還有Redis需要處理過期的鍵,它內部有定時任務去清理過期鍵,那麼既然Redis是單線程的這個任務由誰去執行呢?還是那個處理請求的EventLoop線程,EventLoop線程其實不光處理IO請求,還會處理一些任務和定時任務用來避免鎖(具體可以參考Netty的網路模型)
- 明白NodeJs的高性能,我覺得也是一個應用,在技術選型的過程中不用人云亦云。Java也有拿得出手的框架:Netty
- 採用響應式框架編寫代碼。,在開發響應式代碼中心中也能保持警惕自己所寫的代碼會不會導致EventLoop的阻塞(阻塞EventLoop是相當嚴重的問題)。如果阻塞最好是能通過非同步的api實現業務邏輯,如果避免不了阻塞或者耗時操作,則需要把任務投遞到另外的線程池中去處理,任何情況下都不要去阻塞EventLoop,像VertX框架中如操作Mysql,PostgreSql這種都已經有了非同步的實現。響應式編程一種趨勢,從現在開始擁抱它吧!
- 在學習Dubbo的時候他預設的Rpc協議Dubbo協議底層就是Netty,消費者和提供者之間是單一長連接,所以官網也指出他更適合小數據量大併發,因為單個連接的帶寬上限在7MByte左右。如果要傳輸文件,可以採用Http,這樣的帶寬上限就是物理網卡的上限,Http可以開啟多個連接。
- 上面四條說了Reactor結合非同步的,其實Jdk8中的CompletableFuture是一個非常優秀的非同步實現,我們在需要非同步化邏輯時(比如調用第三方介面)可以充分利用這個類,我曾經也寫過一點關於這個類:非同步編程降低延遲
最後還想說一句,Netty這個框架實在是太強大了,線程模型設計十分優秀,VertX把很多非同步操作委托給了底層的Netty,因為Netty實現中的EventLoop具有天然的線程隔離(一個EventLoop對應一個線程,只會被這個線程調用),很多地方免去了同步,VertX同樣繼承了這個優點,有機會一定好好看看Netty的設計和源碼。
六、困惑
阻塞和同步的四種組合,對於非同步阻塞還是無法理解,這種模式真的存在嗎?
參考文章
- 回調地獄的今生前世
- 怎樣理解阻塞非阻塞與同步非同步的區別? - 嚴肅的回答 - 知乎
- 怎樣理解阻塞非阻塞與同步非同步的區別? - 陳碩的回答 - 知乎
- Netty官網
- IO - 同步,非同步,阻塞,非阻塞
- nodejs真的是單線程嗎?
- 作為一個伺服器,node.js 是性能最高的嗎? - 圓胖腫的回答 - 知乎
- web框架性能排名
- Java8實戰第11章
- Java併發編程實戰
- Netty權威指南第二版