協程這個概念是1963年提出來的,最早的出發點並不是針對併發(那時候的os應該還沒有線程),而是另一種編程模式,相對子例程而言。子例程通俗說就是函數,現在我們寫程序,已經習慣了函數的調用是在棧中,有調用者和被調用者的區別,還有遞歸調用,而協程模型中,各協程雖然看上去是一個函數,但彼此之間是對等的,沒有誰調用誰的關係;子例程的調用由於是一個棧結構,則需要嚴格按照後進先出的方式來調用和返回,而協程的生命週期則是自由的,按照實際需要來結束(返回)
如果查閱一些資料,可能會看到說另一個區別是,協程是一個多入口多出口的子例程,但這個入口和出口跟一般子例程還是有區別,一般子例程是“調用”和“返回”,而一個協程的出入更合適的說法是“切換”,舉個簡單的生產者-消費者的例子(代碼摘自wiki):
var q := new queue
生產者協程:
loop
while q is not full
create some new items
add the items to q
yield to consume
消費者協程:
loop
while q is not empty
remove some items from q
use the items
yield to produce
這裏的yield表示切換協程,生產者和消費者都是一個無限loop,生產者往隊列q裏面儘可能增加新任務,然後切換到消費者執行,消費者處理完q裏面所有任務後,切換回生產者執行,可以拿這個例子和函數調用、線程併發比較下:
如果採用函數調用的方式,上述代碼直接改yield切換爲調用是行不通的,因爲函數調用必須有調用者和被調用者,如果produce裏面調用了consume,consume裏面又去調用produce,則無限遞歸,因此必須考慮實際情況,consume作爲produce的子例程,produce的yield改爲call,consume的yield改爲return,這樣形成一個從屬關係即可,畢竟是要先produce才能consume。函數調用和協程的另一個區別是,雖然每次函數調用都有自己的局部環境,但隨着return切換,局部環境會銷燬,而協程的局部環境是跟協程的生命週期走的,實際上上面這段代碼在一個不支持協程的語言裏面實現的時候,相當於建立兩個協程的數據環境,yield則是直接goto到對應代碼執行
協程和線程很類似,事實上很多時候協程被認爲是用戶態調度的線程,這個說法勉強也算對,但由於協程出現早,如果咬文嚼字的話不應該用線程來描述協程,應該說線程是協程的替代品。考慮上面的例子,如果用線程實現就非常簡單了,比如yield改爲sleep一段時間,主動放棄cpu,os會幫我們在兩個線程切換,或者如果q是一個內核對象,則對q的出入操作可以由os來阻塞調度,這樣代碼就能進一步簡化了
從這個角度說,協程在程序中的實現跟線程在os中的實現是很類似的,一個協程包含一個自己的數據環境(對應線程的棧環境),執行代碼(對應線程的入口函數),聲明週期(線程入口函數的調用和返回)。線程調度依賴os,協程則可以自己調度,但是,要實現自己調度需要一個非常自由的goto(線程調度實際也是切換上下文環境後直接jmp),而在基於函數調用模式的語言中實現協程,還是跟線程的os一樣使用一箇中央調度器。或許唯一的區別是,線程的標準調度是被硬件強行中斷的,而協程是自己交出控制權
和函數,線程類比,則一個協程的“多入口多出口”也能很好的理解,比如如下協程代碼:
func f():
for i from 1 to 10:
return i
如果f是個函數,則上面這段代碼就相當於return 1,但如果是協程,則依次調用f的時候,會返回1到10,也就是說每次return會返回給調用者當前的i,同時當前執行狀態也被保留,下次調用時從上次的狀態繼續,顯然這是一個狀態機,因此協程可以用一個對象實現(python代碼): class F:
def __init__(self):
self.i = 1
def __call__(self):
if self.i > 10:
raise CrEnded()
ret = self.i
self.i += 1
return ret
f = F()
while True:
try:
print f()
except CrEnded:
break
f是一個狀態機,重載它的()運算(__call__),就能像上面一樣反覆調用了,協程最終結束後,再調用會拋出CrEnded異常,當然也可以有其他類型的表示結束的方法。python中的協程(generator生成器)實際就是類似這樣實現的
而如果用線程來實現這個例子,由於線程是os調度的,要想f受控運行,需要通過通訊來控制:
func f():
for i from 1 to 10:
recv_from_queue()
send_to_queue(i)
f在一個線程執行,通過一個queue和外界通訊,每recv到一個請求,就將i send回去。換句話說,線程的多入口多出口是通過通訊和阻塞實現的 協程通過自己主動yield放棄執行來調度,一個協程本質就是一個自動機,雖然對一個自動機而言,整個流程是比較清晰的,但是如果業務比較複雜,手工寫自動機也是比較繁瑣的,比如說,我們要從若干數據庫和表中分別讀取一個用戶的各種信息,組成一個總信息結構,每個信息的讀取可能阻塞的,則需要多次yield,用協程:
user = User(id)
trans = get_info_a(user.id) //提交info a的請求
yield trans
user.info_a = trans.result
... //其他info
這裏爲了寫清楚一些,get_info_a提交請求後返回一個事務對象trans,然後將trans在切換時以“返回值”的形式返回,當結果回來時,外部控制代碼填寫trans.result,然後繼續執行。當然完全不必這麼麻煩,python直接用這種語法: user.info_a = yield get_info_a(user.id)
提交請求並yield返回trans,yield本身是個表達式,其值爲繼續執行時傳入的參數 這樣一來,就可以在協程很方便的寫同步代碼了,但是,外部代碼依然要自己實現爲異步的,僅僅是協程的自動機代碼寫起來緊湊而已,不至於像單線程異步那樣凌亂。而更嚴重的問題是,如果我要實現函數調用,比如一個函數a調用b,a的其他地方和b都可能阻塞,那麼改成協程的話,就不得不建立a和b兩個協程,它倆還是對等的,這樣寫代碼還是比較麻煩,至少不符合現在流行的線程+函數的習慣了