本文原題「從小白到高手,你須要理解同步與異步」,轉載請聯繫做者。php
一、系列文章引言
1.1 文章目的
做爲即時通信技術的開發者來講,高性能、高併發相關的技術概念早就瞭然與胸,什麼線程池、零拷貝、多路複用、事件驅動、epoll等等名詞信手拈來,又或許你對具備這些技術特徵的技術框架好比:Java的Netty、Php的workman、Go的gnet等熟練掌握。但真正到了面視或者技術實踐過程當中遇到沒法釋懷的疑惑時,方知自已所掌握的不過是皮毛。html
返璞歸真、迴歸本質,這些技術特徵背後的底層原理究竟是什麼?如何能通俗易懂、絕不費力真正透徹理解這些技術背後的原理,正是《從根上理解高性能、高併發》系列文章所要分享的。git
1.2 文章源起
我整理了至關多有關IM、消息推送等即時通信技術相關的資源和文章,從最開始的開源IM框架MobileIMSDK,到網絡編程經典鉅著《TCP/IP詳解》的在線版本,再到IM開發綱領性文章《新手入門一篇就夠:從零開發移動端IM》,以及網絡編程由淺到深的《網絡編程懶人入門》、《腦殘式網絡編程入門》、《高性能網絡編程》、《鮮爲人知的網絡編程》系列文章。程序員
越往知識的深處走,越以爲對即時通信技術瞭解的太少。因而後來,爲了讓開發者門更好地從基礎電信技術的角度理解網絡(尤爲移動網絡)特性,我跨專業收集整理了《IM開發者的零基礎通訊技術入門》系列高階文章。這系列文章已然是普通即時通信開發者的網絡通訊技術知識邊界,加上以前這些網絡編程資料,解決網絡通訊方面的知識盲點基本夠用了。github
對於即時通信IM這種系統的開發來講,網絡通訊知識確實很是重要,但迴歸到技術本質,實現網絡通訊自己的這些技術特徵:包括上面提到的線程池、零拷貝、多路複用、事件驅動等等,它們的本質是什麼?底層原理又是怎樣?這就是整理本系列文章的目的,但願對你有用。數據庫
1.3 文章目錄
《從根上理解高性能、高併發(一):深刻計算機底層,理解線程與線程池》編程
《從根上理解高性能、高併發(二):深刻操做系統,理解I/O與零拷貝技術》後端
《從根上理解高性能、高併發(三):深刻操做系統,完全理解I/O多路複用》服務器
《從根上理解高性能、高併發(四):深刻操做系統,完全理解同步與異步》(* 本文)微信
《從根上理解高性能、高併發(五):高併發高性能服務器究竟是如何實現的 (稍後發佈..)》
1.4 本篇概述
接上篇《深刻操做系統,完全理解I/O多路複用》,本篇是高性能、高併發系列的第4篇文章,本篇將從基着眼,爲你講解什麼是同步和異步,以及這兩個極爲重要的概念在高併發、高性能技術中編程中到底意味着什麼。
二、本文做者
應做者要求,不提供真名,也不提供我的照片。
本文做者主要技術方向爲互聯網後端、高併發高性能服務器、檢索引擎技術,網名是「碼農的荒島求生」,公衆號「碼農的荒島求生」。感謝做者的無私分享。
三、寫在前面
相信不少同窗遇到同步異步這兩個詞的時候大腦瞬間就像紅綠燈失靈的十字路口同樣陷入一片懵逼的狀態。
是的,這兩個看上去很像實際上也很像的詞彙曾經給博主形成過很大的困擾,這兩個詞背後所表明的含義究竟是什麼呢?
咱們先從工做場景講起。
四、同步與異步場景1:苦逼的程序員
4.1 同步
假設如今老闆分配給了你一個很緊急而且很重要的任務,讓你下班前必須完成(萬惡的資本主義)。爲了督促進度,老闆搬了個椅子坐在一邊盯着你寫代碼。
你內心確定已經罵上了:「WTF,你有這麼閒嗎?盯着老子,你就不能去幹點其餘事情嗎?」
老闆彷彿接收到了你的腦電波同樣:「我就在這等着,你寫完前我哪也不去,廁所也不去。」
這個例子中老闆交給你任務後就一直等待,什麼都不作直到你寫完,這個場景就是所謂的同步。
4.2 異步
次日,老闆又交給了你一項任務。
不過此次就沒那麼着急啦,此次老闆輕描淡寫,「小夥子能夠啊,不錯不錯,你再努力幹一年,明年我就財務自由了,今天的這個任務不着急,你寫完告訴我一聲就行」。
此次老闆沒有盯着你寫代碼,而是轉身刷視頻去了,你寫完後簡單的和老闆報告一聲「我寫完了」。
在這個例子中老闆交代完任務後再也不一直等着什麼都不作而是就去忙其它事情,你完成任務後簡單的告訴老闆任務完成,這就是所謂的異步。
4.3 小結一下
針對上面的場景,咱們小結一下:在異步這種場景下重點是在你寫代碼的同時老闆在刷劇,這兩件事在同時進行,而不是一方等待另外一方,所以這就是爲何通常來講異步比同步高效的本質所在,無論同步異步應用在什麼場景下。
咱們能夠看到同步這個詞每每和任務的「依賴」、「關聯」、「等待」等關鍵詞相關,而異步每每和任務的「不依賴」,「無關聯」,「無需等待」,「同時發生」等關鍵詞相關。
By the way,若是遇到一個在身後盯着你寫代碼的老闆,三十六計走爲上策。
五、同步與異步場景2:打電話與發郵件
5.1 同步
做爲一名苦逼的程序員是不能只顧埋頭搬磚的,平時工做中的溝通免除不了,其中一種高效的溝通方式是吵架。。。啊不,是電話。
一般打電話時都是一我的在說另外一我的聽,一我的在說的時候另外一我的等待,等另外一我的說完後再接着說,所以在這個場景中你能夠看到,「依賴」、「關聯」、「等待」這些關鍵詞出現了,所以打電話這種溝通方式就是所謂的同步。
5.2 異步
另外一種碼農經常使用的溝通方式是郵件。
郵件是另外一種必不可少溝通方式,由於沒有人傻等着你寫郵件什麼都不作,所以你能夠慢慢悠悠的寫,當你在寫郵件時收件人能夠去作一些像摸摸魚啊、上個廁所、和同時抱怨一下爲何十一假期不放兩週之類有意義的事情。
同時當你寫完郵件發出去後也不須要乾巴巴的等着對方回覆什麼都不作,你也能夠作一些像摸魚之類這樣有意義的事情(^_^)。
在這裏,你寫郵件別人摸魚,這兩件事又在同時進行,收件人和發件人都不須要相互等待,發件人寫完郵件的時候簡單的點個發送就能夠了,收件人收到後就能夠閱讀啦,收件人和發件人不須要相互依賴、不須要相互等待。
你看,在這個場景下「不依賴」,「無關聯」,「無需等待」這些關鍵詞就出現了,所以郵件這種溝通方式就是異步的。
六、編程中的同步調用
如今終於回到編程的主題啦。
既然如今咱們已經理解了同步與異步在各類場景下的意義(I hope so),那麼對於程序員來講該怎樣理解同步與異步呢?
咱們先說同步調用,這是程序員最熟悉的場景。
通常的函數調用都是同步的,就像這樣:
funcA() {
// 等待函數funcB執行完成
funcB();
// 繼續接下來的流程
}
funcA調用funcB,那麼在funcB執行完前,funcA中的後續代碼都不會被執行,也就是說funcA必須等待funcB執行完成。
就像下圖這樣:
從上圖中咱們能夠看到,在funcB運行期間funcA什麼都作不了,這就是典型的同步。
注意:通常來講,像這種同步調用,funcA和funcB是運行在同一個線程中的,這是最爲常見的狀況。
但值得注意的是:即便運行在兩個不能線程中的函數也能夠進行同步調用,像咱們進行IO操做時實際上底層是經過系統調用的方式向操做系統發出請求的,好比磁盤文件讀取:
read(file, buf);
這就是咱們在《深刻操做系統,理解I/O與零拷貝技術》中描述的阻塞式I/O,在read函數返回前程序是沒法繼續向前推動的:
read(file, buf);
// 程序暫停運行,
// 等待文件讀取完成後繼續運行
以下圖所示:
只有當read函數返回後程序才能夠被繼續執行。
注意:和上面的同步調用不一樣的是,函數和被調函數運行在不一樣的線程中。
所以:咱們能夠得出結論,同步調用和函數與被調函數是否運行在同一個線程是沒有關係的。
在這裏咱們還要再次強調:同步方式下函數和被調函數沒法同時進行。
同步編程對程序員來講是最天然最容易理解的。
但容易理解的代價就是在一些場景下,同步並非高效的,緣由很簡單,由於任務沒有辦法同時進行。
接下來咱們看異步調用。
七、編程中的異步調用
有同步調用就有異步調用。
若是你真的理解了本節到目前爲止的內容的話,那麼異步調用對你來講不是問題。
通常來講:異步調用老是和I/O操做等耗時較高的任務如影隨形,像磁盤文件讀寫、網絡數據的收發、數據庫操做等。
咱們仍是以磁盤文件讀取爲例。
在read函數的同步調用方式下,文件讀取完以前調用方是沒法繼續向前推動的,但若是read函數能夠異步調用狀況就不同了。
假如read函數能夠異步調用的話,即便文件尚未讀取完成,read函數也能夠當即返回。
read(file, buff);
// read函數當即返回
// 不會阻塞當前程序
就像下圖這樣:
能夠看到:在異步這種調用方式下,調用方不會被阻塞,函數調用完成後能夠當即執行接下來的程序。
這時異步的重點就在於:調用方接下來的程序執行能夠和文件讀取同時進行,從上圖中咱們也能看出這一點,這就是異步的高效之處。
可是:請注意,異步調用對於程序員來講在理解上是一種負擔,代碼編寫上更是一種負擔,總的來講,上帝在爲你打開一扇門的時候會適當的關上一扇窗戶。
有的同窗可能會問,在同步調用下,調用方再也不繼續執行而是暫停等待,被調函數執行完後很天然的就是調用方繼續執行,那麼異步調用下調用方怎知道被調函數是否執行完成呢?
這就分爲了兩種狀況:
- 1)調用方根本就不關心執行結果;
- 2)調用方須要知道執行結果。
第一種狀況比較簡單,無需討論。
第二種狀況下就比較有趣了,一般有兩種實現方式:
- 1)一種是通知機制:當任務執行完成後發送信號來通知調用方任務完成(這裏的信號有不少實現方式:Linux中的signal,或使用信號量等機制均可實現);
- 2)一種是回調機制:也就是咱們常說的callback(關於回調咱們將在下一篇文章中重點講解,本篇會有簡短的討論)。
接下來咱們用一個具體的例子講解一下同步調用與異步調用。
八、具體的編程例子中理解同步和異步
8.1 一個具體的示例
咱們以常見的Web服務來舉例說明這一問題。
通常來講Web Server接收到用戶請求後會有一些典型的處理邏輯,最多見的就是數據庫查詢(固然,你也能夠把這裏的數據庫查詢換成其它I/O操做,好比磁盤讀取、網絡通訊等),在這裏咱們假定處理一次用戶請求須要通過步驟A、B、C,而後讀取數據庫,數據庫讀取完成後須要通過步驟D、E、F。
就像這樣:
# 處理一次用戶請求須要通過的步驟:
A;
B;
C;
數據庫讀取;
D;
E;
F;
其中:步驟A、B、C和D、E、F不須要任何I/O,也就是說這六個步驟不須要讀取文件、網絡通訊等,涉及到I/O操做的只有數據庫查詢這一步。
通常來講:這樣的Web Server有兩個典型的線程:主線程和數據庫處理線程(注意:這討論的只是典型的場景,具體業務實際上可會有差異,但這並不影響咱們用兩個線程來講明問題)。
首先咱們來看下最簡單的實現方式,也就是同步。
這種方式最爲天然也最爲容易理解:
// 主線程
main_thread() {
A;
B;
C;
發送數據庫查詢請求;
D;
E;
F;
}
// 數據庫線程
DataBase_thread() {
while(1) {
處理數據庫讀取請求;
返回結果;
}
}
這就是最爲典型的同步方法:主線程在發出數據庫查詢請求後就會被阻塞而暫停運行,直到數據庫查詢完畢後面的D、E、F才能夠繼續運行。
就像下圖這樣:
從上圖中咱們能夠看到:主線程中會有「空隙」,這個空隙就是主線程的「休閒時光」,主線程在這段休閒時光中須要等待數據庫查詢完成才能繼續後續處理流程。
在這裏主線程就比如監工的老闆,數據庫線程就比如苦逼搬磚的程序員,在搬完磚前老闆什麼都不作只是牢牢的盯着你,等你搬完磚後纔去忙其它事情。
顯然:高效的程序員是不能容忍主線程偷懶的。
是時候祭出大殺器了,這就是異步:
在異步這種實現方案下主線程根本不去等待數據庫是否查詢完成,而是發送完數據庫讀寫請求後直接處理下一個請求。
有的同窗可能會有疑問:一個請求須要通過A、B、C、數據庫查詢、D、E、F這七個步驟,若是主線程在完成A、B、C、數據庫查詢後直接進行處理接下來的請求,那麼上一個請求中剩下的D、E、F幾個步驟怎麼辦呢?
若是你們尚未忘記上一小節內容的話應該知道,這有兩種狀況,咱們來分別討論。
8.2 異步狀況1:主線程不關心數據庫操做結果
在這種狀況下,主線程根本就不關心數據庫是否查詢完畢,數據庫查詢完畢後自行處理接下來的D、E、F三個步驟。
就像下圖這樣:
看到了吧,接下來重點來了哦。
咱們說過一個請求須要通過七個步驟,其中前三個是在主線程中完成的,後四個是在數據庫線程中完成的,那麼數據庫線程是怎麼知道查完數據庫後要處理D、E、F這幾個步驟呢?
這時,咱們的另外一個主角回調函數就開始登場啦。
沒錯,回調函數就是用來解決這一問題的。
咱們能夠將處理D、E、F這幾個步驟封裝到一個函數中,假定將該函數命名爲handle_DEF_after_DB_query。
僞碼以下:
void handle_DEF_after_DB_query () {
D;
E;
F;
}
這樣主線程在發送數據庫查詢請求的同時將該函數一併當作參數傳遞過去:
DB_query(request, handle_DEF_after_DB_query);
數據庫線程處理完後直接調用handle_DEF_after_DB_query就能夠了,這就是回調函數的做用。
也有的同窗可能會有疑問,爲何這個函數要傳遞給數據庫線程而不是數據庫線程本身定義本身調用呢?
由於從軟件組織結構上講,這不是數據庫線程該作的工做。
數據庫線程須要作的僅僅就是查詢數據庫、而後調用一個處理函數,至於這個處理函數作了些什麼數據庫線程根本就不關心,也不該該關心。
你能夠傳入各類各樣的回調函數:也就是說數據庫系統能夠針對回調函數這一抽象的函數變量來編程,從而更好的應對變化,由於回調函數的內容改變不會影響到數據庫線程的邏輯,而若是數據庫線程本身定義處理函數那麼這種設計就沒有靈活性可言了。
而從軟件開發的角度看:假設數據庫線程邏輯封裝爲了庫提供給其它團隊,當數據庫團隊在研發時怎麼可能知道數據庫查詢後該作什麼呢?
顯然:只有使用方纔知道查詢完數據庫後該作些什麼,所以使用方在使用時簡單的傳入這個回調函數就能夠了。
這樣複雜數據庫的團隊就和使用方團隊實現了所謂的解耦。
如今你應該明白回調函數的做用了吧。
另外:仔細觀察上面兩張圖,你能看出爲何異步比同步高效嗎?
緣由很簡單,這也是咱們在本篇提到過的,異步自然就無需等待,無依賴。
從上一張圖中咱們能夠看到主線程的「休閒時光」不見了,取而代之的是不斷的工做、工做、工做,就像苦逼的996程序員同樣,並且數據庫線程也沒有那麼大段大段的空閒了,取而代之的也是工做、工做、工做。
主線程處理請求和數據庫處理查詢請求能夠同時進行,所以從系統性能上看,這樣的設計能更加充分的利用系統資源,更加快速的處理請求;從用戶的角度看,系統的響應也會更加迅速。
這就是異步的高效之處。
但咱們應該也能夠看出:異步編程並不如同步來的容易理解,系統可維護性上也不如同步模式。
那麼有沒有一種方法既能結合同步模式的容易理解又能結合異步模式的高效呢?答案是確定的,咱們將在後續章節詳細講解這一技術。
接下來咱們看第二種狀況,那就是主線程須要關心數據庫查詢結果。
8.2 異步狀況2:主線程關心數據庫操做結果
在這種狀況下,數據庫線程須要將查詢結果利用通知機制發送給主線程,主線程在接收到消息後繼續處理上一個請求的後半部分。
就像下圖這樣:
從這裏咱們能夠看到:ABCDEF幾個步驟所有在主線中處理,同時主線程一樣也沒有了「休閒時光」,只不過在這種狀況下數據庫線程是比較悠閒的,從這裏並無上一種方法高效,可是依然要比同步模式下要高效。
最後須要注意的是:並非全部的狀況下異步都必定比同步高效,還須要結合具體業務以及IO的複雜度具體狀況具體分析。
九、本文小結
在這篇文章中咱們從各類場景分析了同步與異步這兩個概念,可是無論在什麼場景下,同步每每意味着雙方要相互等待、相互依賴,而異步意味着雙方相互獨立、各行其是。但願本篇能對你們理解這兩個重要的概念有所幫助。
下一篇《從根上理解高性能、高併發(五):高併發高性能服務器究竟是如何實現的》,敬請期待!
附錄:更多高性能、高併發文章精選
《高性能網絡編程(一):單臺服務器併發TCP鏈接數到底能夠有多少》
《高性能網絡編程(二):上一個10年,著名的C10K併發鏈接問題》
《高性能網絡編程(三):下一個10年,是時候考慮C10M併發問題了》
《高性能網絡編程(四):從C10K到C10M高性能網絡應用的理論探索》
《高性能網絡編程(五):一文讀懂高性能網絡編程中的I/O模型》
《高性能網絡編程(六):一文讀懂高性能網絡編程中的線程模型》
《以網遊服務端的網絡接入層設計爲例,理解實時通訊的技術挑戰》
《一套海量在線用戶的移動端IM架構設計實踐分享(含詳細圖文)》
《騰訊資深架構師乾貨總結:一文讀懂大型分佈式系統設計的方方面面》
本文已同步發佈於「即時通信技術圈」公衆號。
▲ 本文在公衆號上的連接是:點此進入,原文連接是:http://www.52im.net/thread-3296-1-1.html