程序員應如何理解協程

做爲程序員,想必你多多少少聽過協程這個詞,這項技術近年來愈來愈多的出如今程序員的視野當中,尤爲高性能高併發領域。當你的同窗、同事提到協程時若是你的大腦一片空白,對其毫無概念。。。python

那麼這篇文章正是爲你量身打造的。程序員

話很少說,今天的主題就是做爲程序員,你應該如何完全理解協程。編程

普通的函數

咱們先來看一個普通的函數,這個函數很是簡單:併發

def func():
print("a")
print("b")
print("c")編程語言

這是一個簡單的普通函數,當咱們調用這個函數時會發生什麼?函數

  1. 調用func
  2. func開始執行,直到return
  3. func執行完成,返回函數A

是否是很簡單,函數func執行直到返回,並打印出:高併發

a
b
c性能

So easy,有沒有,有沒有!spa

很好!操作系統

注意這段代碼是用python寫的,但本篇關於協程的討論適用於任何一門語言,咱們只不過剛好使用了python來用做示例,由於其足夠簡單。

那麼協程是什麼呢?

從普通函數到協程

接下來,咱們就要從普通函數過渡到協程了。

和普通函數只有一個返回點不一樣,協程能夠有多個返回點

這是什麼意思呢?

void func() {
print("a")
暫停並返回
print("b")
暫停並返回
print("c")
}

普通函數下,只有當執行完print("c")這句話後函數纔會返回,可是在協程下當執行完print("a")後func就會因「暫停並返回」這段代碼返回到調用函數。

有的同窗可能會一臉懵逼,這有什麼神奇的嗎?我寫一個return也能返回,就像這樣:

void func() {
print("a")
return
print("b")
暫停並返回
print("c")
}

直接寫一個return語句確實也能返回,但這樣寫的話return後面的代碼都不會被執行到了

協程之因此神奇就神奇在當咱們從協程返回後還能繼續調用該協程,而且是從該協程的上一個返回點後繼續執行

這足夠神奇吧,就比如孫悟空說一聲「定」,函數就被暫停了:

void func() {
print("a")

print("b")

print("c")
}

這時咱們就能夠返回到調用函數,當調用函數何時想起該協程後能夠再次調用該協程,該協程會從上一個返回點繼續執行。

Amazing,有沒有,有沒有!

很是好!

只不過孫大聖使用的口訣「定」字,在編程語言中通常叫作yield(其它語言中可能會有不一樣的實現,但本質都是同樣的)。

須要注意的是,當普通函數返回後,進程的地址空間中不會再保存該函數運行時的任何信息,而協程返回後,函數的運行時信息是須要保存下來的,那麼函數的運行時狀態到底在內存中是什麼樣子呢,關於這個問題你能夠參考_這裏_。

接下來,咱們就用實際的代碼看一看協程。

show me the code

下面咱們使用一個真實的例子來說解,語言採用python,不熟悉的同窗不用擔憂,這裏不會有理解上的門檻。

在python語言中,這個「定」字一樣使用關鍵詞yield,這樣咱們的func函數就變成了:

void func() {
print("a")
yield
print("b")
yield
print("c")
}

注意,這時咱們的func就再也不是簡簡單單的函數了,而是升級成爲了協程,那麼咱們該怎麼使用呢,很簡單:

1 def A():
2   co = func() # 獲得該協程
3   next(co)   # 調用協程
4   print("in function A") # do something
5   next(co)   # 再次調用該協程

咱們看到雖然func函數沒有return語句,也就是說雖然沒有返回任何值,可是咱們依然能夠寫co = func()這樣的代碼,意思是說co就是咱們拿到的協程了。

接下來咱們調用該協程,使用next(co),運行函數A看看執行到第3行的結果是什麼:

a

顯然,和咱們的預期同樣,協程func在print("a")後因執行yield而暫停並返回函數A。

接下來是第4行,這個毫無疑問,A函數在作一些本身的事情,所以會打印:

a
in functino A

接下來是重點的一行,當執行第5行再次調用協程時該打印什麼呢?

若是func是普通函數,那麼會執行func的第一行代碼,也就是打印a。

但func不是普通函數,而是協程,咱們以前說過,協程會在上一個返回點繼續運行,所以這裏應該執行的是func函數第一個yield以後的代碼,也就是print("b")。

a
in functino A
b

看到了吧,協程是一個很神奇的函數,它會本身記住以前的執行狀態,當再次調用時會從上一次的返回點繼續執行。

神奇不神奇,厲害不厲害!

Very Good.

圖形化解釋

爲了讓你更加完全的理解協程,咱們使用圖形化的方式再看一遍,首先是普通的函數調用:

在該圖中,方框內表示該函數的指令序列,若是該函數不調用任何其它函數,那麼應該從上到下依次執行,但函數中能夠調用其它函數,所以其執行並非簡單的從上到下,箭頭線表示執行流的方向。

從圖中咱們能夠看到,咱們首先來到funcA函數,執行一段時間後發現調用了另外一個函數funcB,這時控制轉移到該函數,執行完成後回到main函數的調用點繼續執行。

這是普通的函數調用。

接下來是協程。

在這裏,咱們依然首先在funcA函數中執行,運行一段時間後調用協程,協程開始執行,直到第一個掛起點,此後就像普通函數同樣返回funcA函數,funcA函數執行一些代碼後再次調用該協程,注意,協程這時就和普通函數不同了,協程並非從第一條指令開始執行而是從上一次的掛起點開始執行,執行一段時間後遇到第二個掛起點,這時協程再次像普通函數同樣返回funcA函數,funcA函數執行一段時間後整個程序結束。

函數只是協程的一種特例

怎麼樣,神奇不神奇,和普通函數不一樣的是,協程能知道本身上一次執行到了哪裏

如今你應該明白了吧,協程會在函數被暫停運行時保存函數的運行狀態,並能夠從保存的狀態中恢復並繼續運行。

很熟悉的味道有沒有,這不就是操做系統對線程的調度嘛,線程也能夠被暫停,操做系統保存線程運行狀態而後去調度其它線程,此後該線程再次被分配CPU時還能夠繼續運行,就像沒有被暫停過同樣。

只不過線程的調度是操做系統實現的,這些對程序員都不可見,而協程是在用戶態實現的,對程序員可見。

這就是爲何有的人說能夠把協程理解爲用戶態線程的緣由。

此處應該有掌聲。

也就是說如今程序員能夠扮演操做系統的角色了,你能夠本身控制協程在何時運行,何時暫停,也就是說協程的調度權在你本身手上。

在協程這件事兒上,調度你說了算

當你在協程中寫下yield的時候就是想要暫停改協程,當使用next()時就是要再次運行該協程。

如今你應該理解爲何說函數只是協程的一種特例了吧,函數其實只是沒有掛起點的協程而已。

協程的歷史

有的同窗可能認爲協程是一種比較新的技術,然而其實協程這種概念早在1958就已經提出來了,要知道這時線程的概念都尚未提出來

到了1972年,終於有編程語言實現了這個概念,這兩門編程語言就是Simula 67 以及Scheme。

但協程這個概念始終沒有流行起來,甚至在1993年還有人考古同樣專門寫論文挖出協程這種古老的技術。

由於這一時期尚未線程,若是你想在操做系統寫出併發程序那麼你將不得不使用相似協程這樣的技術,後來線程開始出現,操做系統終於開始原生支持程序的併發執行,就這樣,協程逐漸淡出了程序員的視線。

直到近些年,隨着互聯網的發展,尤爲是移動互聯網時代的到來,服務端對高併發的要求愈來愈高,協程再一次重回技術主流,各大編程語言都已經支持或計劃開始支持協程。

如今你應該對協程有一個清晰的認知了吧。

總結

到這裏你應該已經理解協程究竟是怎麼一回事,可是,依然有一個問題沒有解決,爲何協程這種技術又一次重回視線,協程適用於什麼場景下呢?該怎麼使用呢?

關於這些問題,下一篇文章將會給你答案。

但願這篇對你理解協程有所幫助。

相關文章
相關標籤/搜索