從IO講起java
應用獨佔式python
在計算機發展的初期,每一個應用都是獨佔式的,沒有OS進行調度,每次只加載一個進程,學過單片機的朋友應該有過這樣的體驗,例如經常使用的8086系列芯片,我當時學習微機原理課程是使用仿真軟件Proteus,寫出彙編,編譯成二級制文件,load的仿真軟件上,就能夠運行。一般咱們寫的彙編程序會控制一些基本外設,例如鍵盤,燈,蜂鳴器,定時器之類的,其中比較關鍵的就是中斷,當外設被觸發,會向CPU發出一箇中斷信號,CPU的中斷處理機制,會保護好發起中斷的現場,而後去執行中斷處理函數的地址,處理完之後,會回到剛剛保存的現場。linux
因爲單片機的只是我已經忘得差很少了,就拿一個完整的計算機結構圖來講一說。假設是最原始的計算機,沒有OS或者說咱們的程序就是一個簡單的OS,程序完成的工做是:nginx
1.用戶輸入helloc++
2.從磁盤中讀取world數據庫
3.組合成 hello world並寫入磁盤編程
1和2這個兩個任務實際上是沒有前後順序的,可是在一個進程的世界裏,必需要有前後順序,並不能併發執行,因而整個執行流程就是這樣:CPU進入中斷,程序等待用戶輸入,用戶輸入以後,CPU中斷返回,將IO總線上拿到的hello的字節寫入主存的某個地址,接着發送指令讀取磁盤的world的地址並等待字節返回,而後寫入主存的hello的前一個地址,而後發送指令將hello world對應地址的內容寫入磁盤。後端
OS協調式-爲了併發tomcat
因爲硬件發展,只有一個程序在CPU上跑有些浪費,因而OS做爲一個大管家來協調你們的資源需求,因而抽象了進程的概念,當多個進程併發在一個CPU上跑時,一個被IO阻塞,OS可讓CPU執行別的進程。性能優化
後來因爲進程切換比較消耗CPU,而且也不能資源共享,因而抽象出線程,線程的CPU使用也是由OS協調,OS經過時間片的方法進行強佔式CPU資源分配,程序的編寫者不用關注何時讓出資源,何時執行代碼,全都由OS管理,這時看起來已經很完美了,世界一片明亮。
高併發下的挑戰
有了線程以後,咱們處理併發最直觀的作法就是加線程,爲了減小線程的啓動時間,咱們開始使用線程池,預先啓動一些線程。隨着併發進一步提升,加上外部請求基本上都是IO密集型,使用線程帶來的效益開始降低,也就是說在線程的生命週期中IO等待時間遠遠大於CPU計算時間,另外每一個線程大約須要4M的內存,因爲內存的限制,單機線程數不會不少。因此初期的Apache、tomcat服務器一般只能處理幾千的併發。爲了突破單機下的併發問題,以nginx爲首的一種叫事件驅動的方案開始流行。
爲了代碼好看、好寫
事件驅動其實充分利用了線程,對於有阻塞的操做,就扔過去一個回調,主流程繼續執行,當阻塞的流程執行完成就會調用回調函數。這種異步的方法與以前的同步寫法不同,例如一件事須要1,2,3,4這樣的順序執行,假如這四個步驟都是阻塞的話,就須要三層回調,要是步驟再多一點就會產生回調地域,代碼可讀性不好,還容易寫錯。因而一些語言就出了第三方庫就出來幫忙,聲明出叫作協程的概念,能夠用同步的方式寫異步,例如c++的libgo,java的Quasar,還有一些新穎的語言,直接將這個特性加入官方庫,例如go、python三、kotlin、java11
事件驅動
事件驅動的最初應用是在UI編程上,其中很重要的一點就是須要感知一些外設的操做,從本質上仍是IO,咱們的一次鼠標點擊,鍵盤敲擊,觸摸屏的滑動都是一個事件,會放在OS的隊列中,最初的作法就是專門有個線程去輪序各個隊列看看有沒有相關事件,可是這樣比較浪費CPU資源,因而OS說你應用不要來不斷問我了,你先來告訴我你關注哪些事件類型,等事件發生了我告訴你得了。因而應用的UI線程開開心心等着事件通知,不用再跑着去問OS了。以後這種模型在後端也發揚廣大,下面我麼來舉兩個栗子。
UI方面以Android爲例,在應用啓動時會有建立一個UI主線程,在主線程中會調用Looper.loop方法,該方法是一個死循環,用來更新UI,可是不會卡死,內部使用了linux的epoll機制。Android應用程序的主線程在進入消息循環過程前,會在內部建立一個Linux管道(Pipe),這個管道的做用是使得Android應用程序主線程在消息隊列爲空時能夠進入空閒等待狀態,而且使得當應用程序的消息隊列有消息須要處理時喚醒應用程序的主線程。在線程沒有消息處理時,雖然有死循環,可是經過linux I/O阻塞機制讓程處於空閒狀態,有能力去執行其餘操做,因此不會由於looper死循環致使線程卡死,固然主線程的UI也不會卡頓。
後端方面以Netty爲例, 有一個主線程對應bossEventLoopGroup中的惟一的一個EventLoop,其中也是一個循環,經過NIO的方式(在Linux上底層依然是使用Epoll)或者Epoll的方式,調用操做系統的阻塞方法等待事件到來,而後將事件放入WorkEventLoopGroup的隊列中,等待EventLoop來執行。
netty這個結構可能比較複雜,仍是以處理網絡鏈接爲例,下圖更簡單的描述了事件驅動,用一個線程處理全部的鏈接,這個線程一般是一個循環的方法,當處理一個鏈接遇到阻塞的操做就將任務丟給其它的線程,主線程接着處理下一個鏈接,有沒有感受和Android的UI模型出奇的類似。
對比上面的兩個例子,UI主線程至關於netty中的那個bossEventLoop,一樣適用epoll機制,經過系統調用的阻塞等待事件的到來,以後將事件分發出去,讓相應的handler處理。
看了上面的例子,以爲世界應該很美好了,可是不必定,雖然咱們接受到消息以後將業務邏輯放入Work Thread Pool進行處理,看似能夠同時處理不少請求,可是若是業務處理中也會進行其它的IO操做的話對於整個應用的併發來講是沒有什麼幫助的,由於每一個請求要執行比較長的時間,其中大部分時間用於,讀寫磁盤、等待數據庫,其它接口等IO操做的返回,爲了同時處理更多的請求,咱們只好加線程,這又回到了最初的問題:線程的使用是比較昂貴的。
最好的辦法就是消除阻塞IO,也就將空等的操做所有去除,也就是從底層庫一直到業務代碼所有改造爲異步,可是這對開發者提出了更高的要求,異步的代碼比同步的代碼難寫還難理解,因此這並非理想的解決方案。
協程
前面說到事件驅動雖然能夠經過異步的方式提高效率,可是對開發者的要求也高了,代碼邏輯也不清楚了,那麼有沒有同步的方式來寫非阻塞的代碼呢,固然有,那就是協程。
協程是從本質上講是一種非搶佔式的用戶態線程,結合文章開頭說的獨佔式應用,其實有必定的類似性,那就是都是協做式(非搶佔)的,若是把獨佔式應用看作是單個線程執行,三個步驟看作是三個函數,那就和同步邏輯同樣,與其不一樣的,協程經過非搶佔式的調度來達到併發,簡單的說當一個函數遇到阻塞,會讓出CPU,主動跳到別的沒到阻塞狀態的函數去執行,以次來達到併發的目的,固然若是全部的函數都是純計算(非IO)的,那麼協程並無什麼用處,由於沒有CPU時鐘被浪費。
協程與線程和進程有什麼區別呢,線程和進程是操做系統經過強佔式的調度,強硬把正在使用CPU的線程或進程踢走,讓給別的線程或進程,因爲切換的速度比較快,從而達到感受是併發執行的效果。而協程一般是經過一個線程去運行全部協程方法,每一個協程讓不讓出線程資源本身說了算。
以下面的僞代碼所示,從main方法進入,執行coroutine1,當知足i<10這個條件以後,便讓出CPU,保存此時的上下文信息,進而去執行coroutine2,一樣的coroutine2知足i%2==1也讓出CPU並保存上下文,由於這個程序只有兩個協程,因而又跳回到coroutine1接着以前的上下文繼續執行。
因而對於處理鏈接這樣的事情就變成下圖這樣,每一個協程處理一個鏈接,當阻塞的時候就yield,讓出CPU去執行別的協程,而且因爲上下文切換過程在用戶態執行,花費比較小,因而性能就獲得了提高。
協程看起來如此美好,那咱們快用上協程呀。且慢,你如今項目用的什麼語言,GO?恭喜你,放心的用,GO的整個體系中全部的IO底層庫所有是協程,僅僅一個go關鍵字你就能體會同步編寫異步併發的代碼,體會協程在IO方面的強大。可是若是別的語言,仍是當心爲好,由於用要配合異步IO庫來使用,若是用成同步的庫,完了,你的程序就要hang住了。
二者對比
其實對於事件驅動和協程的對比仍是比較好說的
共性
不一樣點
其實不少技術不是靠幾句話就能說清楚的,在此我給你們推薦一個Java架構方面的交流學習羣:698581634,裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系,主要針對Java開發人員提高本身,突破瓶頸,相信你來學習,會有提高和收穫。在這個羣裏會有你須要的內容 朋友們請抓緊時間加入進來吧。