自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

程序員應如何理解高并發(fā)中的協(xié)程

開發(fā) 架構
作為程序員,想必你多多少少聽過協(xié)程這個詞,這項技術近年來越來越多的出現(xiàn)在程序員的視野當中,尤其高性能高并發(fā)領域。當你的同學、同事提到協(xié)程時如果你的大腦一片空白,對其毫無概念。。。

[[354665]]

作為程序員,想必你多多少少聽過協(xié)程這個詞,這項技術近年來越來越多的出現(xiàn)在程序員的視野當中,尤其高性能高并發(fā)領域。當你的同學、同事提到協(xié)程時如果你的大腦一片空白,對其毫無概念。。。

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

話不多說,今天的主題就是作為程序員,你應該如何徹底理解協(xié)程。

普通的函數(shù)

我們先來看一個普通的函數(shù),這個函數(shù)非常簡單:

  1. def func(): 
  2.    print("a"
  3.    print("b"
  4.    print("c"

這是一個簡單的普通函數(shù),當我們調(diào)用這個函數(shù)時會發(fā)生什么?

調(diào)用func

func開始執(zhí)行,直到return

func執(zhí)行完成,返回函數(shù)A

是不是很簡單,函數(shù)func執(zhí)行直到返回,并打印出:

So easy,有沒有,有沒有!

很好!

注意這段代碼是用python寫的,但本篇關于協(xié)程的討論適用于任何一門語言,因為協(xié)程并不是一種語言的特性。而我們只不過恰好使用了python來用作示例,因其足夠簡單。

那么協(xié)程是什么呢?

從普通函數(shù)到協(xié)程

接下來,我們就要從普通函數(shù)過渡到協(xié)程了。

和普通函數(shù)只有一個返回點不同,協(xié)程可以有多個返回點。

這是什么意思呢?

  1. void func() { 
  2.   print("a"
  3.   暫停并返回 
  4.   print("b"
  5.   暫停并返回 
  6.   print("c"

普通函數(shù)下,只有當執(zhí)行完print("c")這句話后函數(shù)才會返回,但是在協(xié)程下當執(zhí)行完print("a")后func就會因“暫停并返回”這段代碼返回到調(diào)用函數(shù)。

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

  1. void func() { 
  2.   print("a"
  3.   return 
  4.   print("b"
  5.   暫停并返回 
  6.   print("c"

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

協(xié)程之所以神奇就神奇在當我們從協(xié)程返回后還能繼續(xù)調(diào)用該協(xié)程,并且是從該協(xié)程的上一個返回點后繼續(xù)執(zhí)行。

這足夠神奇吧,就好比孫悟空說一聲“定”,函數(shù)就被暫停了:

  1. void func() { 
  2.   print("a"
  3.   定 
  4.   print("b"
  5.   定 
  6.   print("c"

這時我們就可以返回到調(diào)用函數(shù),當調(diào)用函數(shù)什么時候想起該協(xié)程后可以再次調(diào)用該協(xié)程,該協(xié)程會從上一個返回點繼續(xù)執(zhí)行。

Amazing,有沒有,集中注意力,千萬不要翻車。

只不過孫大圣使用的口訣“定”字,在編程語言中一般叫做yield(其它語言中可能會有不同的實現(xiàn),但本質(zhì)都是一樣的)。

需要注意的是,當普通函數(shù)返回后,進程的地址空間中不會再保存該函數(shù)運行時的任何信息,而協(xié)程返回后,函數(shù)的運行時信息是需要保存下來的,那么函數(shù)的運行時狀態(tài)到底在內(nèi)存中是什么樣子呢,關于這個問題你可以參考這里。

接下來,我們就用實際的代碼看一看協(xié)程。

Show Me The Code

下面我們使用一個真實的例子來講解,語言采用python,不熟悉的同學不用擔心,這里不會有理解上的門檻。

在python語言中,這個“定”字同樣使用關鍵詞yield,這樣我們的func函數(shù)就變成了:

  1. void func() { 
  2.   print("a"
  3.   yield 
  4.   print("b"
  5.   yield 
  6.   print("c"

注意,這時我們的func就不再是簡簡單單的函數(shù)了,而是升級成為了協(xié)程,那么我們該怎么使用呢,很簡單:

  1. def A(): 
  2.   co = func() # 得到該協(xié)程 
  3.   next(co)    # 調(diào)用協(xié)程 
  4.   print("in function A") # do something 
  5.   next(co)    # 再次調(diào)用該協(xié)程 

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

接下來我們調(diào)用該協(xié)程,使用next(co),運行函數(shù)A看看執(zhí)行到第3行的結果是什么:

顯然,和我們的預期一樣,協(xié)程func在print("a")后因執(zhí)行yield而暫停并返回函數(shù)A。

接下來是第4行,這個毫無疑問,A函數(shù)在做一些自己的事情,因此會打?。?/p>

  1. in function A 

接下來是重點的一行,當執(zhí)行第5行再次調(diào)用協(xié)程時該打印什么呢?

如果func是普通函數(shù),那么會執(zhí)行func的第一行代碼,也就是打印a。

但func不是普通函數(shù),而是協(xié)程,我們之前說過,協(xié)程會在上一個返回點繼續(xù)運行,因此這里應該執(zhí)行的是func函數(shù)第一個yield之后的代碼,也就是print("b")。

  1. in function A 

看到了吧,協(xié)程是一個很神奇的函數(shù),它會自己記住之前的執(zhí)行狀態(tài),當再次調(diào)用時會從上一次的返回點繼續(xù)執(zhí)行。

圖形化解釋

為了讓你更加徹底的理解協(xié)程,我們使用圖形化的方式再看一遍,首先是普通的函數(shù)調(diào)用:

在該圖中,方框內(nèi)表示該函數(shù)的指令序列,如果該函數(shù)不調(diào)用任何其它函數(shù),那么應該從上到下依次執(zhí)行,但函數(shù)中可以調(diào)用其它函數(shù),因此其執(zhí)行并不是簡單的從上到下,箭頭線表示執(zhí)行流的方向。

從圖中我們可以看到,我們首先來到funcA函數(shù),執(zhí)行一段時間后發(fā)現(xiàn)調(diào)用了另一個函數(shù)funcB,這時控制轉(zhuǎn)移到該函數(shù),執(zhí)行完成后回到main函數(shù)的調(diào)用點繼續(xù)執(zhí)行。

這是普通的函數(shù)調(diào)用。

接下來是協(xié)程。

在這里,我們依然首先在funcA函數(shù)中執(zhí)行,運行一段時間后調(diào)用協(xié)程,協(xié)程開始執(zhí)行,直到第一個掛起點,此后就像普通函數(shù)一樣返回funcA函數(shù),funcA函數(shù)執(zhí)行一些代碼后再次調(diào)用該協(xié)程,注意,協(xié)程這時就和普通函數(shù)不一樣了,協(xié)程并不是從第一條指令開始執(zhí)行而是從上一次的掛起點開始執(zhí)行,執(zhí)行一段時間后遇到第二個掛起點,這時協(xié)程再次像普通函數(shù)一樣返回funcA函數(shù),funcA函數(shù)執(zhí)行一段時間后整個程序結束。

函數(shù)只是協(xié)程的一種特例

怎么樣,神奇不神奇,和普通函數(shù)不同的是,協(xié)程能知道自己上一次執(zhí)行到了哪里。

現(xiàn)在你應該明白了吧,協(xié)程會在函數(shù)被暫停運行時保存函數(shù)的運行狀態(tài),并可以從保存的狀態(tài)中恢復并繼續(xù)運行。

很熟悉的味道有沒有,這不就是操作系統(tǒng)對線程的調(diào)度嘛,線程也可以被暫停,操作系統(tǒng)保存線程運行狀態(tài)然后去調(diào)度其它線程,此后該線程再次被分配CPU時還可以繼續(xù)運行,就像沒有被暫停過一樣。

只不過線程的調(diào)度是操作系統(tǒng)實現(xiàn)的,這些對程序員都不可見,而協(xié)程是在用戶態(tài)實現(xiàn)的,對程序員可見。

這就是為什么有的人說可以把協(xié)程理解為用戶態(tài)線程的原因。

此處應該有掌聲。

也就是說現(xiàn)在程序員可以扮演操作系統(tǒng)的角色了,你可以自己控制協(xié)程在什么時候運行,什么時候暫停,也就是說協(xié)程的調(diào)度權在你自己手上。

在協(xié)程這件事兒上,調(diào)度你說了算。

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

現(xiàn)在你應該理解為什么說函數(shù)只是協(xié)程的一種特例了吧,函數(shù)其實只是沒有掛起點的協(xié)程而已。

協(xié)程的歷史

有的同學可能認為協(xié)程是一種比較新的技術,然而其實協(xié)程這種概念早在1958年就已經(jīng)提出來了,要知道這時線程的概念都還沒有提出來。

到了1972年,終于有編程語言實現(xiàn)了這個概念,這兩門編程語言就是Simula 67 以及Scheme。

[[354669]]

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

因為這一時期還沒有線程,如果你想在操作系統(tǒng)寫出并發(fā)程序那么你將不得不使用類似協(xié)程這樣的技術,后來線程開始出現(xiàn),操作系統(tǒng)終于開始原生支持程序的并發(fā)執(zhí)行,就這樣,協(xié)程逐漸淡出了程序員的視線。

直到近些年,隨著互聯(lián)網(wǎng)的發(fā)展,尤其是移動互聯(lián)網(wǎng)時代的到來,服務端對高并發(fā)的要求越來越高,協(xié)程再一次重回技術主流,各大編程語言都已經(jīng)支持或計劃開始支持協(xié)程。

那么協(xié)程到底是如何實現(xiàn)的呢?

協(xié)程是如何實現(xiàn)的

讓我們從問題的本質(zhì)出發(fā)來思考這個問題。

協(xié)程的本質(zhì)是什么呢?

其實就是可以被暫停以及可以被恢復運行的函數(shù)。

那么可以被暫停以及可以被恢復意味著什么呢?

看過籃球比賽的同學想必都知道(沒看過的也能知道),籃球比賽也是可以被隨時暫停的,暫停時大家需要記住球在哪一方,各自的站位是什么,等到比賽繼續(xù)的時候大家回到各自的位置,裁判哨子一響比賽繼續(xù),就像比賽沒有被暫停過一樣。

看到問題的關鍵了嗎,比賽之所以可以被暫停也可以繼續(xù)是因為比賽狀態(tài)被記錄下來了(站位、球在哪一方),這里的狀態(tài)就是計算機科學中常說的上下文,context。

回到協(xié)程。

協(xié)程之所以可以被暫停也可以繼續(xù),那么一定要記錄下被暫停時的狀態(tài),也就是上下文,當繼續(xù)運行的時候要恢復其上下文(狀態(tài)),那么接下來很自然的一個問題就是,函數(shù)運行時的狀態(tài)是什么?

這個關鍵的問題的答案就在《函數(shù)運行起來后在內(nèi)存中是什么樣子的》這篇文章中,函數(shù)運行時所有的狀態(tài)信息都位于函數(shù)運行時棧中。

函數(shù)運行時棧就是我們需要保存的狀態(tài),也就是所謂的上下文,如圖所示:

從圖中我們可以看出,該進程中只有一個線程,棧區(qū)中有四個棧幀,main函數(shù)調(diào)用A函數(shù),A函數(shù)調(diào)用B函數(shù),B函數(shù)調(diào)用C函數(shù),當C函數(shù)在運行時整個進程的狀態(tài)就如圖所示。

現(xiàn)在我們已經(jīng)知道了函數(shù)的運行時狀態(tài)就保存在棧區(qū)的棧幀中,接下來重點來了哦。

既然函數(shù)的運行時狀態(tài)保存在棧區(qū)的棧幀中,那么如果我們想暫停協(xié)程的運行就必須保存整個棧幀的數(shù)據(jù),那么我們該將整個棧幀中的數(shù)據(jù)保存在哪里呢?

想一想這個問題,整個進程的內(nèi)存區(qū)中哪一塊是專門用來長時間(進程生命周期)存儲數(shù)據(jù)的?是不是大腦又一片空白了?

先別空白!

很顯然,這就是堆區(qū)啊,heap,我們可以將棧幀保存在堆區(qū)中,那么我們該怎么在堆區(qū)中保存數(shù)據(jù)呢?希望你還沒有暈,在堆區(qū)中開辟空間就是我們常用的C語言中的malloc或者C++中的new。

我們需要做的就是在堆區(qū)中申請一段空間,讓后把協(xié)程的整個棧區(qū)保存下,當需要恢復協(xié)程的運行時再從堆區(qū)中copy出來恢復函數(shù)運行時狀態(tài)。

再仔細想一想,為什么我們要這么麻煩的來回copy數(shù)據(jù)呢?

實際上,我們需要做的是直接把協(xié)程的運行需要的棧幀空間直接開辟在堆區(qū)中,這樣都不用來回copy數(shù)據(jù)了,如圖所示。

從圖中我們可以看到,該程序中開啟了兩個協(xié)程,這兩個協(xié)程的棧區(qū)都是在堆上分配的,這樣我們就可以隨時中斷或者恢復協(xié)程的執(zhí)行了。

有的同學可能會問,那么進程地址空間最上層的棧區(qū)現(xiàn)在的作用是什么呢?

這一區(qū)域依然是用來保存函數(shù)棧幀的,只不過這些函數(shù)并不是運行在協(xié)程而是普通線程中的。

現(xiàn)在你應該看到了吧,在上圖中實際上有3個執(zhí)行流:

  1. 一個普通線程
  2. 兩個協(xié)程

雖然有3個執(zhí)行流但我們創(chuàng)建了幾個線程呢?

一個線程。

現(xiàn)在你應該明白為什么要使用協(xié)程了吧,使用協(xié)程理論上我們可以開啟無數(shù)并發(fā)執(zhí)行流,只要堆區(qū)空間足夠,同時還沒有創(chuàng)建線程的開銷,所有協(xié)程的調(diào)度、切換都發(fā)生在用戶態(tài),這就是為什么協(xié)程也被稱作用戶態(tài)線程的原因所在。

掌聲在哪里?

因此即使你創(chuàng)建了N多協(xié)程,但在操作系統(tǒng)看來依然只有一個線程,也就是說協(xié)程對操作系統(tǒng)來說是不可見的。

這也許是為什么協(xié)程這個概念比線程提出的要早的原因,可能是寫普通應用的程序員比寫操作系統(tǒng)的程序員最先遇到需要多個并行流的需求,那時可能都還沒有操作系統(tǒng)的概念,或者操作系統(tǒng)沒有并行這種需求,所以非操作系統(tǒng)程序員只能自己動手實現(xiàn)執(zhí)行流,也就是協(xié)程。

現(xiàn)在你應該對協(xié)程有一個清晰的認知了吧。

總結

到這里你應該已經(jīng)理解協(xié)程到底是怎么一回事了,但是,依然有一個問題沒有解決,為什么協(xié)程這種技術又一次重回視線,協(xié)程適用于什么場景下呢?該怎么使用呢?

本文轉(zhuǎn)載自微信公眾號「 碼農(nóng)的荒島求生」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系 碼農(nóng)的荒島求生公眾號。

 

責任編輯:武曉燕 來源: 碼農(nóng)的荒島求生
相關推薦

2022-10-25 08:23:09

Reactor模式I/O

2024-06-27 07:56:49

2021-05-13 21:58:00

高并發(fā)應用Asyncio

2011-07-25 09:14:40

程序員

2011-05-31 09:22:39

程序員

2024-12-03 15:15:22

2016-10-28 17:39:47

phpgolangcoroutine

2011-05-26 10:04:30

程序員

2011-03-22 10:16:48

程序員

2023-11-24 11:15:21

協(xié)程編程

2011-05-24 13:47:25

程序員

2011-05-30 14:50:56

程序員

2010-07-27 16:21:25

程序員

2011-06-02 09:02:36

程序員

2011-05-31 09:37:53

程序員

2011-06-02 09:56:21

程序員團隊精神

2023-10-12 09:46:00

并發(fā)模型線程

2020-02-24 10:39:55

Python函數(shù)線程池

2021-09-16 09:59:13

PythonJavaScript代碼

2015-03-16 15:33:11

JavaScript程序員應備素質(zhì)
點贊
收藏

51CTO技術棧公眾號