版權(quán)歸原作者所有,如有侵權(quán),請(qǐng)聯(lián)系我們

關(guān)于協(xié)程,你了解多少?

中移科協(xié)
原創(chuàng)
有用的科技知識(shí)又增加了
收藏

隨著異步編程的發(fā)展以及各種并發(fā)框架的普及,協(xié)程作為一種異步編程規(guī)范在各類(lèi)語(yǔ)言中地位逐步提高。我們不單單會(huì)在自己的程序中使用協(xié)程,各類(lèi)框架如fastapi,aiohttp等也都是基于異步以及協(xié)程進(jìn)行實(shí)現(xiàn)。那到底什么是協(xié)程?協(xié)程是怎么發(fā)展來(lái)的呢?本文將會(huì)對(duì)這些問(wèn)題做一個(gè)深入淺出的介紹。

1、進(jìn)程,線(xiàn)程到協(xié)程

1.1 概念

眾所周知,計(jì)算機(jī)操作系統(tǒng)中有兩個(gè)常見(jiàn)的概念:進(jìn)程和線(xiàn)程。要講協(xié)程,我們先從這兩個(gè)基本概念入手。

進(jìn)程:操作系統(tǒng)中每一個(gè)獨(dú)立允許的程序,都會(huì)占有操作系統(tǒng)分配的資源,是資源分配的基本單位。進(jìn)程之間互不干涉,都只負(fù)責(zé)運(yùn)行自己的指令,這就是進(jìn)程。

線(xiàn)程進(jìn)程中的一個(gè)實(shí)體,是被系統(tǒng)獨(dú)立調(diào)度和CPU分派資源的基本單位,線(xiàn)程自己不擁有系統(tǒng)資源,只擁有一些運(yùn)行時(shí)必不可少的資源,如自己的堆棧,程序計(jì)數(shù)器,寄存器數(shù)據(jù)等。一個(gè)進(jìn)程可以有多個(gè)線(xiàn)程,各個(gè)線(xiàn)程共享進(jìn)程所擁有的全部資源。

協(xié)程:協(xié)作的線(xiàn)程,也可以被稱(chēng)作微線(xiàn)程,是一種用戶(hù)態(tài)的線(xiàn)程,協(xié)程的調(diào)度是由用戶(hù)主動(dòng)完成的。代表了一種非搶占式的多任務(wù)并發(fā)的調(diào)度思想:協(xié)作式調(diào)度,即沒(méi)有優(yōu)先級(jí)高低的區(qū)分。

1.2 對(duì)比

①?gòu)膬?nèi)存占用,上下文切換內(nèi)容,上下文切換過(guò)程等角度進(jìn)行詳細(xì)對(duì)比。


②從包容關(guān)系上來(lái)說(shuō),一個(gè)進(jìn)程至少包含一個(gè)線(xiàn)程,一個(gè)線(xiàn)程里面有0個(gè)或者多個(gè)協(xié)程,因此可歸納為如下圖:

2、從異步編程說(shuō)起

異步編程,也可以叫做并發(fā)編程,并發(fā)不同于并行:并行是物理上并行,至少要有2個(gè)CPU,兩個(gè)線(xiàn)程同時(shí)運(yùn)行;而并發(fā)可以是單核,通過(guò)時(shí)間調(diào)度算法實(shí)現(xiàn)多任務(wù)調(diào)度,給人感覺(jué)是同時(shí)運(yùn)行,實(shí)際上某一時(shí)刻只有一個(gè)線(xiàn)程在運(yùn)行。異步編程能有效避免主線(xiàn)程被阻塞,特別是對(duì)于前端來(lái)說(shuō),如果主線(xiàn)程被阻塞,會(huì)導(dǎo)致APP無(wú)響應(yīng)。常見(jiàn)的異步編程有:多線(xiàn)程,回調(diào),Promise,響應(yīng)式編程以及協(xié)程。

2.1 多線(xiàn)程

以發(fā)微博來(lái)舉例,發(fā)布操作可以簡(jiǎn)單歸結(jié)為如下三個(gè)操作:

①獲取用戶(hù)簽名數(shù)據(jù)prepareSubmit

②攜帶簽名數(shù)據(jù)進(jìn)行微博發(fā)布內(nèi)容提交請(qǐng)求postSubmit

③處理請(qǐng)求,響應(yīng)結(jié)果processPost

最開(kāi)始我們可能只有10個(gè)用戶(hù),只需要啟動(dòng)10個(gè)線(xiàn)程去操作,但是隨著用戶(hù)數(shù)增加到1000個(gè),10000個(gè),這個(gè)時(shí)候如果啟動(dòng)10000個(gè)線(xiàn)程,由于每個(gè)線(xiàn)程至少會(huì)占用4M,10000個(gè)線(xiàn)程會(huì)占用39G的內(nèi)存,對(duì)服務(wù)器的性能要求太高了,并且線(xiàn)程之間的切換也會(huì)占用大量的系統(tǒng)時(shí)間。因此這種方式只適用于線(xiàn)程之間沒(méi)有競(jìng)爭(zhēng)關(guān)系,占用內(nèi)存資源少,切對(duì)時(shí)延不敏感的情況。

2.2 回調(diào)

如果用異步回調(diào)的等方式解決上面發(fā)微博的問(wèn)題,我們可以用如下代碼來(lái)解決,這種方式簡(jiǎn)單易懂,使用范圍也很廣,幾乎所有的異步框架都用到了回調(diào)。但是也有很明顯的問(wèn)題:

①如果步驟很多就會(huì)出現(xiàn)嵌套地獄

②對(duì)于異常的情況很難處理和傳遞

③如果某一個(gè)步驟要等多個(gè)回調(diào)完成之后再進(jìn)行收口操作,也很困難

2.3 Promise

Promise是說(shuō)對(duì)于一個(gè)耗時(shí)比較久的操作,程序給你一個(gè)承諾,保證不久之后會(huì)把結(jié)果告知你。它采用了鏈?zhǔn)骄幊棠P?,?jiǎn)化了回調(diào)的異步操作,解決了嵌套地獄的問(wèn)題,Promise有以下幾種狀態(tài):

①待定(pending): 初始狀態(tài),既沒(méi)有被兌現(xiàn),也沒(méi)有被拒絕。

②兌現(xiàn)(fulfilled): 操作成功完成。

③拒絕(rejected): 操作失敗。

發(fā)微博問(wèn)題使用promise來(lái)解決如圖,必須等前置條件兌現(xiàn)之后才往后。

Promise存在如下問(wèn)題:

①每一步的返回值類(lèi)型都必須是 Promise,不能是實(shí)際的數(shù)據(jù)類(lèi)型

②錯(cuò)誤處理變得復(fù)雜,不同階段產(chǎn)生的錯(cuò)誤很難一路傳遞下去

③不同階段之間共享數(shù)據(jù)困難

2.4 響應(yīng)式編程

響應(yīng)式編程(Reactive Extension簡(jiǎn)稱(chēng)Rx)的核心是將一切當(dāng)作數(shù)據(jù)流,關(guān)注數(shù)據(jù)流的變換和流轉(zhuǎn),描述數(shù)據(jù)輸入與輸出之間的關(guān)系,會(huì)實(shí)現(xiàn)數(shù)量眾多的擴(kuò)展函數(shù),這些函數(shù)只對(duì)輸入和輸出負(fù)責(zé),因此可以很輕松的將函數(shù)分發(fā)到其他線(xiàn)程上實(shí)現(xiàn)異步調(diào)用。但Rx調(diào)試比較困難,學(xué)習(xí)成本較高,維護(hù)也不易。

2.5 協(xié)程

考慮到大部分互聯(lián)網(wǎng)請(qǐng)求都是IO密集型而不是CPU密集型,基本的流程都是:請(qǐng)求-少量計(jì)算-調(diào)用公共服務(wù)-大量讀寫(xiě)數(shù)據(jù)庫(kù)-返回?cái)?shù)據(jù)。因此IO密集型很容易發(fā)生讀寫(xiě)阻塞,此時(shí)會(huì)進(jìn)行線(xiàn)程切換,執(zhí)行其他線(xiàn)程。但線(xiàn)程是寶貴的計(jì)算資源,因此我們希望線(xiàn)程不要阻塞,一直跑,不要切換上下文。針對(duì)這種需求,協(xié)程的優(yōu)勢(shì)就出來(lái)了。協(xié)程執(zhí)行如圖:

協(xié)程存在以下優(yōu)勢(shì):

①協(xié)程的創(chuàng)建,銷(xiāo)毀和調(diào)度都發(fā)生在用戶(hù)態(tài),避免CPU頻繁切換帶來(lái)的資源浪費(fèi)

②內(nèi)存占用小,可以輕松創(chuàng)建幾十萬(wàn)的協(xié)程

③可讀性高,易維護(hù),代碼基本等同于同步

④通過(guò)結(jié)構(gòu)化并發(fā)限制控制域,減少內(nèi)存泄漏

3、種類(lèi)劃分
3.1 按照調(diào)用棧分類(lèi)
協(xié)程最關(guān)鍵的步驟就是暫停代碼和恢復(fù)代碼執(zhí)行,實(shí)現(xiàn)方法主要基于棧和狀態(tài)機(jī)&閉包兩種。通過(guò)區(qū)分執(zhí)行協(xié)程的時(shí)候是否可以在任意嵌套函數(shù)中被掛起,可以分為有棧協(xié)程和無(wú)棧協(xié)程,有棧協(xié)程可以被掛起,無(wú)棧協(xié)程不能被掛起。先看正常的函數(shù)棧操作:

有棧協(xié)程

協(xié)程實(shí)現(xiàn)的關(guān)鍵點(diǎn)就是如何保存、恢復(fù)和切換上下文,如果將一個(gè)函數(shù)當(dāng)作協(xié)程,當(dāng)有棧協(xié)程對(duì)函數(shù)的上下文進(jìn)行保存,恢復(fù)和切換操作時(shí),會(huì)對(duì)這個(gè)函數(shù)及其嵌套函數(shù),棧針存儲(chǔ)的值,寄存器存儲(chǔ)的值進(jìn)行快照操作,之后只需要對(duì)快照做,恢復(fù)和切換。

無(wú)棧協(xié)程

相比于有棧協(xié)程,無(wú)棧協(xié)程在不改變調(diào)用棧的情況下采用了類(lèi)似狀態(tài)機(jī)和閉包的方式來(lái)存儲(chǔ)暫停點(diǎn)的代碼信息。在不改變函數(shù)調(diào)用棧的情況下,我們也不可能在任意一個(gè)嵌套函數(shù)中掛起協(xié)程,這也是無(wú)棧協(xié)程的特點(diǎn),同時(shí)由于不需要切換棧幀,無(wú)棧協(xié)程的性能比有棧協(xié)程還要高一點(diǎn)。

3.2 按照調(diào)度方式分類(lèi)

協(xié)程的暫停和恢復(fù)涉及到控制權(quán)的轉(zhuǎn)移,可以分為非對(duì)稱(chēng)協(xié)程和對(duì)稱(chēng)協(xié)程。

非對(duì)稱(chēng)協(xié)程

非對(duì)稱(chēng)協(xié)程通過(guò)暫停和繼續(xù)兩個(gè)指令進(jìn)行控制權(quán)轉(zhuǎn)移,暫停之后控制權(quán)就會(huì)轉(zhuǎn)移給繼續(xù)指令所在的協(xié)程,因此控制權(quán)的轉(zhuǎn)移存在較弱的調(diào)用方和被調(diào)用方的關(guān)系。

對(duì)稱(chēng)協(xié)程

對(duì)稱(chēng)協(xié)程只有一個(gè)繼續(xù)指令,各協(xié)程之間地位是平等的,繼續(xù)指令執(zhí)行之后,控制權(quán)就會(huì)在多個(gè)協(xié)程之間流轉(zhuǎn)。

4、總結(jié)

在高并發(fā)、高請(qǐng)求當(dāng)?shù)赖慕裉欤侠砝脜f(xié)程勢(shì)必會(huì)提升我們的系統(tǒng)性能和用戶(hù)體驗(yàn)。當(dāng)我們的業(yè)務(wù)操作或者網(wǎng)絡(luò)請(qǐng)求面臨大量IO時(shí),我們可以考慮采用協(xié)程替換線(xiàn)程,能夠幫助我們的應(yīng)用降低系統(tǒng)內(nèi)存占用,同時(shí)也減少了系統(tǒng)切換開(kāi)銷(xiāo),提升系統(tǒng)性能。然而協(xié)程雖然很強(qiáng)大,但是也不要過(guò)度使用,協(xié)程只有和異步IO結(jié)合起來(lái)才能發(fā)揮出最大的威力。

作者:李佳斌

單位:中國(guó)移動(dòng)智慧家庭運(yùn)營(yíng)中心