協(xié)程,被我玩會(huì)了!
時(shí)間:2021-11-03 14:52:38
手機(jī)看文章
掃描二維碼
隨時(shí)隨地手機(jī)看文章
[導(dǎo)讀]前言大家好,我的朋友們!干了這么年后端,寫過C/C、Python、Go,每次說到協(xié)程的時(shí)候,腦海里就只能浮現(xiàn)一些關(guān)鍵字yeild、async、go等等。但是對(duì)于協(xié)程這個(gè)知識(shí)點(diǎn),我理解的一直比較模糊,于是決定搞清楚。全文閱讀預(yù)計(jì)耗時(shí)10分鐘,少刷幾個(gè)小視頻的時(shí)間,多學(xué)點(diǎn)知識(shí),想想就...
前言大家好,我的朋友們!干了這么年后端,寫過C/C 、Python、Go,每次說到協(xié)程的時(shí)候,腦海里就只能浮現(xiàn)一些關(guān)鍵字yeild、async、go等等。但是對(duì)于協(xié)程這個(gè)知識(shí)點(diǎn),我理解的一直比較模糊,于是決定搞清楚。全文閱讀預(yù)計(jì)耗時(shí)10分鐘,少刷幾個(gè)小視頻的時(shí)間,多學(xué)點(diǎn)知識(shí),想想就很劃算噻!
協(xié)程概念的誕生
先拋一個(gè)粗淺的結(jié)論:協(xié)程從廣義來說是一種設(shè)計(jì)理念,我們常說的只是具體的實(shí)現(xiàn)。理解好思想,技術(shù)點(diǎn)就很簡(jiǎn)單了,關(guān)于協(xié)程道與術(shù)的區(qū)別:上古神器COBOL
協(xié)程概念的出現(xiàn)比線程更早,甚至可以追溯到20世紀(jì)50年代,提協(xié)程就必須要說到一門生命力極強(qiáng)的最早的高級(jí)編程語言COBOL。最開始我以為COBOL這門語言早就消失在歷史長(zhǎng)河中,但是我錯(cuò)了。COBOL語言,是一種面向過程的高級(jí)程序設(shè)計(jì)語言,主要用于數(shù)據(jù)處理,是國(guó)際上應(yīng)用最廣泛的一種高級(jí)語言。COBOL是英文Common Business-Oriented Language的縮寫,原意是面向商業(yè)的通用語言。
截止到今年在全球范圍內(nèi)大約有1w臺(tái)大型機(jī)中有3.8w 遺留系統(tǒng)中約2000億行代碼是由COBOL寫的,占比高達(dá)65%,同時(shí)在美國(guó)很多政府和企業(yè)機(jī)構(gòu)都是基于COBOL打造的,影響力巨大。時(shí)間拉回1958年,美國(guó)計(jì)算機(jī)科學(xué)家梅爾文·康威(Melvin Conway)就開始鉆研基于磁帶存儲(chǔ)的COBOL的編譯器優(yōu)化問題,這在當(dāng)時(shí)是個(gè)非常熱門的話題,不少青年才俊都撲進(jìn)去了,包括圖靈獎(jiǎng)得主唐納德·爾文·克努斯教授(Donald Ervin Knuth)也寫了一個(gè)優(yōu)化后的編譯器。看看這兩位的簡(jiǎn)介,我沉默了:
梅爾文·康威(Melvin Conway)也是一位超級(jí)大佬,著名的康威定律提出者。
唐納德·爾文·克努斯是算法和程序設(shè)計(jì)技術(shù)的先驅(qū)者,1974年的圖靈獎(jiǎng)得主,計(jì)算機(jī)排版系統(tǒng)TeX和字型設(shè)計(jì)系統(tǒng)METAFONT的發(fā)明者,他因這些成就和大量創(chuàng)造性的影響深遠(yuǎn)的著作而譽(yù)滿全球,《計(jì)算機(jī)程序設(shè)計(jì)的藝術(shù)》被《美國(guó)科學(xué)家》雜志列為20世紀(jì)最重要的12本物理科學(xué)類專著之一。那究竟是什么問題讓這群天才們投入這么大的精力呢?快來看看!
COBOL編譯器的技術(shù)難題
我們都是知道高級(jí)編程語言需要借助編譯器來生成二進(jìn)制可執(zhí)行文件,編譯器的基本步驟包括:讀取字符流、詞法分析、語法分析、語義分析、代碼生成器、代碼優(yōu)化器等。這種管道式的流程,上一步的輸出作為下一步的輸入,將中間結(jié)果存儲(chǔ)在內(nèi)存即可,這在現(xiàn)代計(jì)算機(jī)上毫無壓力,但是受限于軟硬件水平,在幾十年前的COBOL語言卻是很難的。在1958年的時(shí)候,當(dāng)時(shí)的存儲(chǔ)還不發(fā)達(dá),磁帶作為存儲(chǔ)器是1951年在計(jì)算機(jī)中得到應(yīng)用的,所以那個(gè)時(shí)代的COBOL很依賴于磁帶。其實(shí),我在網(wǎng)上找了很多資料去看當(dāng)時(shí)的編譯器有什么問題,只找到了一條:編譯器無法做到讀一次磁帶就可以完成整個(gè)編譯過程,也就是所謂的one-pass編譯器還沒有產(chǎn)生。當(dāng)時(shí)的COBOL程序被寫在一個(gè)磁帶上,而磁帶不支持隨機(jī)讀寫,只能順序讀,而當(dāng)時(shí)的內(nèi)存又不可能把整個(gè)磁帶的內(nèi)容都裝進(jìn)去,所以一次讀取沒編譯完就要再從頭讀。于是,我腦補(bǔ)了COBOL編譯器和磁帶之間可能的兩種multi-pass形式的交互情況:
- 可能情況一
對(duì)于COBOL的編譯器來說,要完成詞法分析、語法分析就要從磁帶上讀取程序的源代碼,在之前的編譯器中詞法分析和語法分析是相互獨(dú)立的,這就意味著: - 詞法分析時(shí)需要將磁帶從頭到尾過一遍
- 語法分析時(shí)需要將磁帶從頭到尾過一遍
- 可能情況二
聽過磁帶的朋友們一定知道磁帶的兩個(gè)基本操作:倒帶和快進(jìn)。
在完成編譯器的詞法分析和語法分析兩件事情時(shí),需要磁帶反復(fù)的倒帶和快進(jìn)去尋找兩類分析所需的部分,類似于磁盤的尋道,磁頭需要反復(fù)移動(dòng)橫跳,并且當(dāng)時(shí)的磁帶不一定支持隨機(jī)讀寫。
協(xié)同式解決方案
在梅爾文·康威的編譯器設(shè)計(jì)中將詞法分析和語法分析合作運(yùn)行,而不再像其他編譯器那樣相互獨(dú)立,兩個(gè)模塊交織運(yùn)行,編譯器的控制流在詞法分析和語法分析之間來回切換:- 當(dāng)詞法分析模塊基于詞素產(chǎn)生足夠多的詞法單元Token時(shí)就控制流轉(zhuǎn)給語法分析
- 當(dāng)語法分析模塊處理完所有的詞法單元Token時(shí)將控制流轉(zhuǎn)給詞法分析模塊
- 詞法分析和語法分析各自維護(hù)自身的運(yùn)行狀態(tài),并且具備主動(dòng)讓出和恢復(fù)的能力
梅爾文·康威構(gòu)建的這種協(xié)同工作機(jī)制,需要參與者讓出(yield)控制流時(shí),記住自身狀態(tài),以便在控制流返回時(shí)能從上次讓出的位置恢復(fù)(resume)執(zhí)行。簡(jiǎn)言之,協(xié)程的全部精神就在于控制流的主動(dòng)讓出和恢復(fù)
。
這種協(xié)作式的任務(wù)流和計(jì)算機(jī)中斷非常像,在當(dāng)時(shí)條件的限制下,由梅爾文·康威提出的這種讓出/恢復(fù)模式的協(xié)作程序被認(rèn)為是最早的協(xié)程概念,并且基于這種思想可以打造新的COBOL編譯器。在1963年,梅爾文·康威也發(fā)表了一篇論文來說明自己的這種思想,雖然半個(gè)多世紀(jì)過去了,有幸我還是找到了這篇論文:https://melconway.com/Home/pdf/compiler.pdf說實(shí)話這paper真是有點(diǎn)難,時(shí)間過于久遠(yuǎn),很難有共鳴,最后我放棄了,要不然我或許能搞明白之前編譯器的具體問題了。
懷才不遇的協(xié)程
雖然協(xié)程概念出現(xiàn)的時(shí)間比線程還要早,但是協(xié)程一直都沒有正是登上舞臺(tái),真是有點(diǎn)懷才不遇的趕腳。我們上學(xué)的時(shí)候,老師就講過一些軟件設(shè)計(jì)思想,其中主流語言崇尚自頂向下top-down的編程思想:對(duì)要完成的任務(wù)進(jìn)行分解,先對(duì)最高層次中的問題進(jìn)行定義、設(shè)計(jì)、編程和測(cè)試,而將其中未解決的問題作為一個(gè)子任務(wù)放到下一層次中去解決。
這樣逐層、逐個(gè)地進(jìn)行定義、設(shè)計(jì)、編程和測(cè)試,直到所有層次上的問題均由實(shí)用程序來解決,就能設(shè)計(jì)出具有層次結(jié)構(gòu)的程序。C語言就是典型的top-down思想的代表,在main函數(shù)作為入口,各個(gè)模塊依次形成層次化的調(diào)用關(guān)系,同時(shí)各個(gè)模塊還有下級(jí)的子模塊,同樣有層次調(diào)用關(guān)系。但是協(xié)程這種相互協(xié)作調(diào)度的思想和top-down是不合的,在協(xié)程中各個(gè)模塊之間存在很大的耦合關(guān)系,并不符合高內(nèi)聚低耦合的編程思想,相比之下top-down使程序結(jié)構(gòu)清晰、層次調(diào)度明確,代碼可讀性和維護(hù)性都很不錯(cuò)。與線程相比,協(xié)作式任務(wù)系統(tǒng)讓調(diào)用者自己來決定什么時(shí)候讓出,比操作系統(tǒng)的搶占式調(diào)度所需要的時(shí)間代價(jià)要小很多,后者為了能恢復(fù)現(xiàn)場(chǎng)會(huì)在切換線程時(shí)保存相當(dāng)多的狀態(tài),并且會(huì)非常頻繁地進(jìn)行切換,資源消耗更大。綜合來說,協(xié)程完全是用戶態(tài)的行為,由程序員自己決定什么時(shí)候讓出控制權(quán),保存現(xiàn)場(chǎng)和切換恢復(fù)使用的資源也非常少,同時(shí)對(duì)提高處理器效率來說也是完全符合的。那么不禁要問:協(xié)程看著不錯(cuò),為啥沒成為主流呢?
- 協(xié)程的思想和當(dāng)時(shí)的主流不符合
- 搶占式的線程可以解決大部分的問題,讓使用者感受的痛點(diǎn)不足
其實(shí),協(xié)程雖然在x86架構(gòu)上沒有折騰出大風(fēng)浪,由于搶占式任務(wù)系統(tǒng)依賴于CPU硬件的支持,對(duì)硬件要求比較高,對(duì)于一些嵌入式設(shè)備來說,協(xié)同調(diào)度再合適不過了,所以協(xié)程在另外一個(gè)領(lǐng)域也施展了拳腳。
協(xié)程的雄起
我們對(duì)于CPU的壓榨從未停止。對(duì)于CPU來說,任務(wù)分為兩大類:計(jì)算密集型和IO密集型。計(jì)算密集型已經(jīng)可以最大程度發(fā)揮CPU的作用,但是IO密集型一直是提高CPU利用率的難點(diǎn)。IO密集型任務(wù)之痛
對(duì)于IO密集型任務(wù),在搶占式調(diào)度中也有對(duì)應(yīng)的解決方案:異步 回調(diào)。也就是遇到IO阻塞,比如下載圖片時(shí)會(huì)立即返回,等待下載完成將結(jié)果進(jìn)行回調(diào)處理,交付給發(fā)起者。就像你常去早餐店,油條還沒好,你和老板很熟悉就先交了錢去座位玩手機(jī)了,等你的油條好了,服務(wù)員就端過去了,這就是典型的異步 回調(diào)。雖然異步 回調(diào)在現(xiàn)實(shí)生活中看著也很簡(jiǎn)單,但是在程序設(shè)計(jì)上卻很讓人頭痛,在某些場(chǎng)景下會(huì)讓整個(gè)程序的可讀性非常差,而且也不好寫,相反同步IO雖然效率低,但是很好寫,還是以為異步圖片下載為例,圖片服務(wù)中臺(tái)提供了異步接口,發(fā)起者請(qǐng)求之后立即返回,圖片服務(wù)此時(shí)給了發(fā)起者一個(gè)唯一標(biāo)識(shí)ID,等圖片服務(wù)完成下載后把結(jié)果放到一個(gè)消息隊(duì)列,此時(shí)需要發(fā)起者不斷消費(fèi)這個(gè)MQ才能拿到下載結(jié)果。整個(gè)過程相比同步IO來說,原來整體的邏輯被拆分為好幾個(gè)部分,各個(gè)子部分有狀態(tài)的遷移,對(duì)大部分程序員來說維護(hù)狀態(tài)簡(jiǎn)直就是噩夢(mèng),日后必然是bug的高發(fā)地。
用戶態(tài)協(xié)同調(diào)度
隨著網(wǎng)絡(luò)技術(shù)的發(fā)展和高并發(fā)要求,對(duì)于搶占式調(diào)度對(duì)IO型任務(wù)處理的低效逐漸受到重視,終于協(xié)程的機(jī)會(huì)來了。協(xié)程將IO的處理權(quán)交給了程序員,遇到IO被阻塞時(shí)就交出控制權(quán)給其他協(xié)程,等其他協(xié)程處理完再把控制權(quán)交回來。通過yield方式轉(zhuǎn)移執(zhí)行權(quán)的多個(gè)協(xié)程之間并非調(diào)用者和被調(diào)用者的關(guān)系,而是彼此平等、對(duì)稱、合作的關(guān)系。協(xié)程一直沒有占上風(fēng)的原因,除了設(shè)計(jì)思想的矛盾,還有一些其他原因,畢竟協(xié)程也不是銀彈,來看看協(xié)程有什么問題:- 協(xié)程無法利用多核,需要配合進(jìn)程來使用才可以在多CPU上發(fā)揮作用
- 線程的回調(diào)機(jī)制仍然有巨大生命力,協(xié)程無法全部替代
- 控制權(quán)需要轉(zhuǎn)移可能造成某些協(xié)程的饑餓,搶占式更加公平
- 協(xié)程的控制權(quán)由用戶態(tài)決定可能轉(zhuǎn)移給某些惡意的代碼,搶占式由操作系統(tǒng)來調(diào)度更加安全
擁抱協(xié)程的編程語言
網(wǎng)絡(luò)操作、文件操作、數(shù)據(jù)庫操作、消息隊(duì)列操作等重IO操作,是任何高級(jí)編程語言無法避開的問題,也是提高程序效率的關(guān)鍵。像Java、C/C 、Python這些老牌語言也陸續(xù)開始借助于第三方包來支持協(xié)程,來解決自身語言的不足。像Golang這種新生選手,在語言層面原生支持了協(xié)程,可以說是徹底擁抱協(xié)程,這也造就了Go的高并發(fā)能力。我們來分別看看它們是怎么實(shí)現(xiàn)協(xié)程的,以及實(shí)現(xiàn)協(xié)程的關(guān)鍵點(diǎn)是什么。Python
Python對(duì)協(xié)程的支持也經(jīng)歷了多個(gè)版本,從部分支持到完善支持一直在演進(jìn):- Python2.x對(duì)協(xié)程的支持比較有限,生成器yield實(shí)現(xiàn)了一部分但不完全
- 第三方庫gevent對(duì)協(xié)程的實(shí)現(xiàn)有比較好,但不是官方的
- Python3.4加入了asyncio模塊
- 在Python3.5中又提供了async/await語法層面的支持
- Python3.6中asyncio模塊更加完善和穩(wěn)
- Python3.7開始async/await成為保留關(guān)鍵字
import?asyncio
from?pathlib?import?Path
import?logging
from?urllib.request?import?urlopen,?Request
import?os
from?time?import?time
import?aiohttp
?
logging.basicConfig(level=logging.INFO,?format='%(asctime)s?-?%(name)s?-?%(levelname)s?-?%(message)s')
logger?=?logging.getLogger(__name__)
?
?
CODEFLEX_IMAGES_URLS?=?['https://codeflex.co/wp-content/uploads/2021/01/pandas-dataframe-python-1024x512.png',
????????????????????????'https://codeflex.co/wp-content/uploads/2021/02/github-actions-deployment-to-eks-with-kustomize-1024x536.jpg',
????????????????????????'https://codeflex.co/wp-content/uploads/2021/02/boto3-s3-multipart-upload-1024x536.jpg',
????????????????????????'https://codeflex.co/wp-content/uploads/2018/02/kafka-cluster-architecture.jpg',
????????????????????????'https://codeflex.co/wp-content/uploads/2016/09/redis-cluster-topology.png']
?
?
async?def?download_image_async(session,?dir,?img_url):
????download_path?=?dir?/?os.path.basename(img_url)
????async?with?session.get(img_url)?as?response:
????????with?download_path.open('wb')?as?f:
????????????while?True:
????????????????chunk?=?await?response.content.read(512)
????????????????if?not?chunk:
????????????????????break
????????????????f.write(chunk)
????logger.info('Downloaded:?'? ?img_url)
?
?
async?def?main():
????images_dir?=?Path("codeflex_images")
????Path("codeflex_images").mkdir(parents=False,?exist_ok=True)
?
????async?with?aiohttp.ClientSession()?as?session:
????????tasks?=?[(download_image_async(session,?images_dir,?img_url))?for?img_url?in?CODEFLEX_IMAGES_URLS]
????????await?asyncio.gather(*tasks,?return_exceptions=True)
?
?
if?__name__?==?'__main__':
????start?=?time()
?????
????event_loop?=?asyncio.get_event_loop()
????try:
????????event_loop.run_until_complete(main())
????finally:
????????event_loop.close()
?
????logger.info('Download?time:?%s?seconds',?time()?-?start)
這段代碼展示了如何使用async/await來實(shí)現(xiàn)圖片的并發(fā)下載功能。- 在普通的函數(shù)def前面加async關(guān)鍵字就變成異步/協(xié)程函數(shù),調(diào)用該函數(shù)并不會(huì)運(yùn)行,而是返回一個(gè)協(xié)程對(duì)象,后續(xù)在event_loop中執(zhí)行
- await表示等待task執(zhí)行完成,也就是yeild讓出控制權(quán),同時(shí)asyncio使用事件循環(huán)event_loop來實(shí)現(xiàn)整個(gè)過程,await需要在async標(biāo)注的函數(shù)中使用
- event_loop事件循環(huán)充當(dāng)管理者的角色,將控制權(quán)在幾個(gè)協(xié)程函數(shù)之間切換
C
在C 20引入?yún)f(xié)程框架,但是很不成熟,換句話說是給寫協(xié)程庫的大佬用的最底層的東西,用起來就很復(fù)雜門檻比較高。C 作為高性能服務(wù)器開發(fā)語言的無冕之王,各大公司也做了很多嘗試來使用協(xié)程功能,比如boost.coroutine、微信的libco、libgo、云風(fēng)用C實(shí)現(xiàn)的協(xié)程庫等。說實(shí)話,C 協(xié)程相關(guān)的東西有點(diǎn)復(fù)雜,后面專門寫一下,在此不展開了。Go
go中的協(xié)程被稱為goroutine,被認(rèn)為是用戶態(tài)更輕量級(jí)的線程,協(xié)程對(duì)操作系統(tǒng)而言是透明的,也就是操作系統(tǒng)無法直接調(diào)度協(xié)程,因此必須有個(gè)中間層來接管goroutine。goroutine仍然是基于線程來實(shí)現(xiàn)的,因?yàn)榫€程才是CPU調(diào)度的基本單位,在go語言內(nèi)部維護(hù)了一組數(shù)據(jù)結(jié)構(gòu)和N個(gè)線程,協(xié)程的代碼被放進(jìn)隊(duì)列中來由線程來實(shí)現(xiàn)調(diào)度執(zhí)行,這就是著名的GMP模型。- G:Goroutine
每個(gè)Gotoutine對(duì)應(yīng)一個(gè)G結(jié)構(gòu)體,G存儲(chǔ)Goroutine的運(yùn)行堆棧,狀態(tài),以及任務(wù)函數(shù),可重用函數(shù)實(shí)體G需要保存到P的隊(duì)列或者全局隊(duì)列才能被調(diào)度執(zhí)行。
- M:machine
M是線程的抽象,代表真正執(zhí)行計(jì)算的資源,在綁定有效的P后,進(jìn)入調(diào)度執(zhí)行循環(huán),M會(huì)從P的本地隊(duì)列來執(zhí)行,
- P:Processor
P是一個(gè)抽象的概念,不是物理上的CPU而是表示邏輯處理器。當(dāng)一個(gè)P有任務(wù),需要?jiǎng)?chuàng)建或者喚醒一個(gè)系統(tǒng)線程M去處理它隊(duì)列中的任務(wù)。
P決定同時(shí)執(zhí)行的任務(wù)的數(shù)量,GOMAXPROCS限制系統(tǒng)線程執(zhí)行用戶層面的任務(wù)的數(shù)量。
對(duì)M來說,P提供了相關(guān)的執(zhí)行環(huán)境,入內(nèi)存分配狀態(tài),任務(wù)隊(duì)列等。GMP模型運(yùn)行的基本過程:
- 首先創(chuàng)建一個(gè)G對(duì)象,然后G被保存在P的本地隊(duì)列或者全局隊(duì)列
- 這時(shí)P會(huì)喚醒一個(gè)M,M尋找一個(gè)空閑的P將G移動(dòng)到它自己,然后M執(zhí)行一個(gè)調(diào)度循環(huán):調(diào)用G對(duì)象->執(zhí)行->清理線程->繼續(xù)尋找Goroutine。
- 在M的執(zhí)行過程中,上下文切換隨時(shí)發(fā)生。當(dāng)切換發(fā)生,任務(wù)的執(zhí)行現(xiàn)場(chǎng)需要被保護(hù),這樣在下一次調(diào)度執(zhí)行可以進(jìn)行現(xiàn)場(chǎng)恢復(fù)。
- M的棧保存在G對(duì)象,只有現(xiàn)場(chǎng)恢復(fù)需要的寄存器(SP,PC等),需要被保存到G對(duì)象。