程序員應如何理解高併發中的協程

做爲程序員,想必你多多少少聽過協程這個詞,這項技術近年來愈來愈多的出如今程序員的視野當中,尤爲高性能高併發領域。當你的同窗、同事提到協程時若是你的大腦一片空白,對其毫無概念。。。
那麼這篇文章正是爲你量身打造的。
話很少說,今天的主題就是做爲程序員,你應該如何完全理解協程。

普通的函數javascript

咱們先來看一個普通的函數,這個函數很是簡單:
def func(): print("a") print("b") print("c")
這是一個簡單的普通函數,當咱們調用這個函數時會發生什麼?
  1. 調用func
  2. func開始執行,直到return
  3. func執行完成,返回函數A
是否是很簡單,函數func執行直到返回,並打印出:
abc
So easy,有沒有,有沒有!
很好!
注意這段代碼是用python寫的,但本篇關於協程的討論適用於任何一門語言由於協程並非一種語言的特性。而咱們只不過剛好使用了python來用做示例,因其足夠簡單。
那麼協程是什麼呢?

從普通函數到協程java

接下來,咱們就要從普通函數過渡到協程了。
和普通函數只有一個返回點不一樣,協程能夠有多個返回點
這是什麼意思呢?
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 Codepython

下面咱們使用一個真實的例子來說解,語言採用python,不熟悉的同窗不用擔憂,這裏不會有理解上的門檻。
在python語言中,這個「定」字一樣使用關鍵詞yield,這樣咱們的func函數就變成了:
void func() { print("a") yield print("b") yield print("c")}
注意,這時咱們的func就再也不是簡簡單單的函數了,而是升級成爲了協程,那麼咱們該怎麼使用呢,很簡單:
def A():  co = func() # 獲得該協程  next(co)    # 調用協程  print("in function A"# do something  next(co)    # 再次調用該協程
們看到雖然func函數沒有return語句,也就是說雖然沒有返回任何值,可是咱們依然能夠寫co = func()這樣的代碼,意思是說co就是咱們拿到的協程了。
接下來咱們調用該協程,使用next(co),運行函數A看看執行到第3行的結果是什麼:
a
顯然,和咱們的預期同樣,協程func在print("a")後因執行yield而暫停並返回函數A。
接下來是第4行,這個毫無疑問,A函數在作一些本身的事情,所以會打印:
ain function A
接下來是重點的一行,當執行第5行再次調用協程時該打印什麼呢?
若是func是普通函數,那麼會執行func的第一行代碼,也就是打印a。
但func不是普通函數,而是協程,咱們以前說過,協程會在上一個返回點繼續運行,所以這裏應該執行的是func函數第一個yield以後的代碼,也就是print("b")。
ain function Ab
看到了吧,協程是一個很神奇的函數,它會本身記住以前的執行狀態,當再次調用時會從上一次的返回點繼續執行。

圖形化解釋程序員

爲了讓你更加完全的理解協程,咱們使用圖形化的方式再看一遍,首先是普通的函數調用:
在該圖中,方框內表示該函數的指令序列,若是該函數不調用任何其它函數,那麼應該從上到下依次執行,但函數中能夠調用其它函數,所以其執行並非簡單的從上到下,箭頭線表示執行流的方向。
從圖中咱們能夠看到,咱們首先來到funcA函數,執行一段時間後發現調用了另外一個函數funcB,這時控制轉移到該函數,執行完成後回到main函數的調用點繼續執行。
這是普通的函數調用。
接下來是協程。
在這裏,咱們依然首先在funcA函數中執行,運行一段時間後調用協程,協程開始執行,直到第一個掛起點,此後就像普通函數同樣返回funcA函數,funcA函數執行一些代碼後再次調用該協程,注意,協程這時就和普通函數不同了,協程並非從第一條指令開始執行而是 從上一次的掛起點開始執行 ,執行一段時間後遇到第二個掛起點,這時協程再次像普通函數同樣返回funcA函數,funcA函數執行一段時間後整個程序結束。

函數只是協程的一種特例編程

怎麼樣,神奇不神奇,和普通函數不一樣的是,協程能知道本身上一次執行到了哪裏
如今你應該明白了吧,協程會在函數被暫停運行時保存函數的運行狀態,並能夠從保存的狀態中恢復並繼續運行。
很熟悉的味道有沒有,這不就是操做系統對線程的調度嘛,線程也能夠被暫停,操做系統保存線程運行狀態而後去調度其它線程,此後該線程再次被分配CPU時還能夠繼續運行,就像沒有被暫停過同樣。
只不過線程的調度是操做系統實現的,這些對程序員都不可見,而協程是在用戶態實現的,對程序員可見。
這就是爲何有的人說能夠把協程理解爲用戶態線程的緣由。
此處應該有掌聲。
也就是說如今程序員能夠扮演操做系統的角色了,你能夠本身控制協程在何時運行,何時暫停,也就是說協程的調度權在你本身手上。
在協程這件事兒上,調度你說了算
當你在協程中寫下yield的時候就是想要暫停該協程,當使用next()時就是要再次運行該協程。
如今你應該理解爲何說函數只是協程的一種特例了吧,函數其實只是沒有掛起點的協程而已。

協程的歷史swift

有的同窗可能認爲協程是一種比較新的技術,然而其實協程這種概念早在1958年就已經提出來了,要知道這時線程的概念都尚未提出來
到了1972年,終於有編程語言實現了這個概念,這兩門編程語言就是Simula 67 以及Scheme。

但協程這個概念始終沒有流行起來,甚至在1993年還有人考古同樣專門寫論文挖出協程這種古老的技術。
由於這一時期尚未線程,若是你想在操做系統寫出併發程序那麼你將不得不使用相似協程這樣的技術,後來線程開始出現,操做系統終於開始原生支持程序的併發執行,就這樣,協程逐漸淡出了程序員的視線。
直到近些年,隨着互聯網的發展,尤爲是移動互聯網時代的到來,服務端對高併發的要求愈來愈高,協程再一次重回技術主流,各大編程語言都已經支持或計劃開始支持協程。
那麼協程究竟是如何實現的呢?

協程是如何實現的ruby

讓咱們從問題的本質出發來思考這個問題。
協程的本質是什麼呢?
其實就是能夠被暫停以及能夠被恢復運行的函數。
那麼能夠被暫停以及能夠被恢復意味着什麼呢?
看過籃球比賽的同窗想必都知道(沒看過的也能知道),籃球比賽也是能夠被隨時暫停的,暫停時你們須要記住球在哪一方,各自的站位是什麼,等到比賽繼續的時候你們回到各自的位置,裁判哨子一響比賽繼續,就像比賽沒有被暫停過同樣。
看到問題的關鍵了嗎,比賽之因此能夠被暫停也能夠繼續是由於比賽狀態被記錄下來了(站位、球在哪一方),這裏的狀態就是計算機科學中常說的上下文,context。
回到協程。
協程之因此能夠被暫停也能夠繼續,那麼必定要記錄下被暫停時的狀態,也就是上下文,當繼續運行的時候要恢復其上下文(狀態),那麼接下來很天然的一個問題就是,函數運行時的狀態是什麼?
這個關鍵的問題的答案就在《函數運行起來後在內存中是什麼樣子的》這篇文章中,函數運行時全部的狀態信息都位於函數運行時棧中。
函數運行時棧就是咱們須要保存的狀態,也就是所謂的上下文,如圖所示:
從圖中咱們能夠看出,該進程中只有一個線程,棧區中有四個棧幀,main函數調用A函數,A函數調用B函數,B函數調用C函數,當C函數在運行時整個進程的狀態就如圖所示。
如今咱們已經知道了函數的運行時狀態就保存在棧區的棧幀中,接下來重點來了哦。
既然函數的運行時狀態保存在棧區的棧幀中,那麼若是咱們想暫停協程的運行就必須保存整個棧幀的數據,那麼咱們該將整個棧幀中的數據保存在哪裏呢?
想想這個問題,整個進程的內存區中哪一塊是專門用來長時間(進程生命週期)存儲數據的?是否是大腦又一片空白了?
先別空白!
很顯然,這就是堆區啊,heap,咱們能夠將棧幀保存在堆區中,那麼咱們該怎麼在堆區中保存數據呢?但願你尚未暈,在堆區中開闢空間就是咱們經常使用的C語言中的malloc或者C++中的new。
咱們須要作的就是在堆區中申請一段空間,讓後把協程的整個棧區保存下,當須要恢復協程的運行時再從堆區中copy出來恢復函數運行時狀態。
再仔細想想,爲何咱們要這麼麻煩的來回copy數據呢?
實際上,咱們須要作的是直接把協程的運行須要的棧幀空間直接開闢在堆區中,這樣都不用來回copy數據了,如圖所示。
從圖中咱們能夠看到,該程序中開啓了兩個協程,這兩個協程的棧區都是在堆上分配的,這樣咱們就能夠隨時中斷或者恢復協程的執行了。
有的同窗可能會問,那麼進程地址空間最上層的棧區如今的做用是什麼呢?
這一區域依然是用來保存函數棧幀的,只不過這些函數並非運行在協程而是普通線程中的。
如今你應該看到了吧,在上圖中實際上有3個執行流:
  1. 一個普通線程服務器

  2. 兩個協程
雖然有3個執行流但咱們建立了幾個線程呢?
一個線程
如今你應該明白爲何要使用協程了吧,使用協程理論上咱們能夠開啓無數併發執行流,只要堆區空間足夠,同時尚未建立線程的開銷,全部協程的調度、切換都發生在用戶態,這就是爲何協程也被稱做用戶態線程的緣由所在。
掌聲在哪裏?
所以即便你建立了N多協程,但在操做系統看來依然只有一個線程,也就是說協程對操做系統來講是不可見的。
這也許是爲何協程這個概念比線程提出的要早的緣由,多是寫普通應用的程序員比寫操做系統的程序員最早遇到須要多個並行流的需求,那時可能都尚未操做系統的概念,或者操做系統沒有並行這種需求,因此非操做系統程序員只能本身動手實現執行流,也就是協程。
如今你應該對協程有一個清晰的認知了吧。


總結微信

到這裏你應該已經理解協程究竟是怎麼一回事了,可是,依然有一個問題沒有解決,爲何協程這種技術又一次重回視線,協程適用於什麼場景下呢?該怎麼使用呢?
關於這些問題,下一篇文章將會給你答案。
最後的最後,若是以爲文章對你有幫助的話,但願多多分享轉發、多多"在看"。
讓咱們一塊兒成爲技術大牛

長按識別二維碼關注 併發

碼農的荒島求生

往期精選

看完這篇還不懂線程與線程池你來打我
讀取文件時,程序經歷了什麼?
終於明白了,一文完全理解I/O多路複用
從小白到高手,你須要理解同步與異步
程序員應如何完全理解回調函數
高性能高併發服務器是如何實現的
函數運行時在內存中是什麼樣子的

掃碼關注

碼農的荒島求生

本文分享自微信公衆號 - 碼農的荒島求生(escape-it)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索