1. 程式人生 > >【計算機內功心法】九:程式設計師應如何理解協程

【計算機內功心法】九:程式設計師應如何理解協程

作為程式設計師,想必你多多少少聽過協程這個詞,這項技術近年來越來越多的出現在程式設計師的視野當中,尤其高效能高併發領域。當你的同學、同事提到協程時如果你的大腦一片空白,對其毫無概念。。。

as-seen-on-tv-celebutard-emoticons-irl-reaction-guys-sham-wow-3001602560

那麼這篇文章正是為你量身打造的。

話不多說,今天的主題就是作為程式設計師,你應該如何徹底理解協程。

普通的函式

我們先來看一個普通的函式,這個函式非常簡單:

def func():
   print("a")
   print("b")
   print("c")

這是一個簡單的普通函式,當我們呼叫這個函式時會發生什麼?

  1. 呼叫func
  2. func開始執行,直到return
  3. func執行完成,返回函式A

是不是很簡單,函式func執行直到返回,並打印出:

a
b
c

So easy,有沒有,有沒有!

ezgif-7-65a8d730506f

很好!

注意這段程式碼是用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,有沒有,有沒有!

ezgif-7-65a8d730506f

非常好!

只不過孫大聖使用的口訣“定”字,在程式語言中一般叫做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

看到了吧,協程是一個很神奇的函式,它會自己記住之前的執行狀態,當再次呼叫時會從上一次的返回點繼續執行。

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

ezgif-7-65a8d730506f

Very Good.

圖形化解釋

為了讓你更加徹底的理解協程,我們使用圖形化的方式再看一遍,首先是普通的函式呼叫:

1606454128466

在該圖中,方框內表示該函式的指令序列,如果該函式不呼叫任何其它函式,那麼應該從上到下依次執行,但函式中可以呼叫其它函式,因此其執行並不是簡單的從上到下,箭頭線表示執行流的方向。

從圖中我們可以看到,我們首先來到funcA函式,執行一段時間後發現呼叫了另一個函式funcB,這時控制轉移到該函式,執行完成後回到main函式的呼叫點繼續執行。

這是普通的函式呼叫。

接下來是協程。

1606454275481

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

1606454374384

函式只是協程的一種特例

怎麼樣,神奇不神奇,和普通函式不同的是,協程能知道自己上一次執行到了哪裡。

現在你應該明白了吧,協程會在函式被暫停執行時儲存函式的執行狀態,並可以從儲存的狀態中恢復並繼續執行。

很熟悉的味道有沒有,這不就是作業系統對執行緒的排程嘛,執行緒也可以被暫停,作業系統儲存執行緒執行狀態然後去排程其它執行緒,此後該執行緒再次被分配CPU時還可以繼續執行,就像沒有被暫停過一樣。

只不過執行緒的排程是作業系統實現的,這些對程式設計師都不可見,而協程是在使用者態實現的,對程式設計師可見。

這就是為什麼有的人說可以把協程理解為使用者態執行緒的原因。

此處應該有掌聲。

a

也就是說現在程式設計師可以扮演作業系統的角色了,你可以自己控制協程在什麼時候執行,什麼時候暫停,也就是說協程的排程權在你自己手上。

在協程這件事兒上,排程你說了算。

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

現在你應該理解為什麼說函式只是協程的一種特例了吧,函式其實只是沒有掛起點的協程而已。

協程的歷史

有的同學可能認為協程是一種比較新的技術,然而其實協程這種概念早在1958就已經提出來了,要知道這時執行緒的概念都還沒有提出來。

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

1920px-Simula_-_logo.svg

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

因為這一時期還沒有執行緒,如果你想在作業系統寫出併發程式那麼你將不得不使用類似協程這樣的技術,後來執行緒開始出現,作業系統終於開始原生支援程式的併發執行,就這樣,協程逐漸淡出了程式設計師的視線。

直到近些年,隨著網際網路的發展,尤其是移動網際網路時代的到來,服務端對高併發的要求越來越高,協程再一次重回技術主流,各大程式語言都已經支援或計劃開始支援協程。

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

gaijin4koma2_peersblog_1200684608

總結

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

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

希望這篇對你理解協程有所幫助。