引自我在知乎上的回答:進程 線程 協程 例程 過程 的區別是什么? - 駿馬金龍的回答 - 知乎
首先解釋下程序、進程、上下文切換和線程。然后再解釋協程、例程、過程。
程序
程序:源代碼堆起來的東西。相當於一個一動不動沒有生命的機器人。
- 雖然是沒有生命的機器人,但是它被設計后就表示有了硬件,它的硬件決定了之后它有生命后是如何干活的
- 機器人有優劣,所以有些優秀的機器人干活很快,而有些機器人干活很慢
進程
進程:程序在系統上跑起來(運行)之后的東西(動態的)。相當於有了生命的機器人。生命是內核給的,動起來的能力是CPU提供的驅動力。
- 因為在操作系統看來,它已經有了生命,會賦予它一些屬性,比如它叫什么(PID),誰發明的(PPID),它在哪個范圍內活動(地址空間).................
- 內核會記錄這個機器人的信息
- 機器人可以造機器人(父子進程)
- 它可以中斷或睡眠。比如機器人想充電,但是沒電給它,它得等
- 內核可以跟它交流,比如傳遞一些數據給它、傳遞信號給它
- 它可以被毀掉。比如進程崩潰或正常退出或被信號終止
- 機器人毀掉后要為它收屍,如果機器人偷偷死掉,就會沒人給它收屍而變成僵屍進程
- 嚴格地說,這個機器人運行起來之后雖然有了生命,但是沒有靈魂,只有CPU給它驅動力的那一段時間,他才能動起來,其它時候都是在哪里睡覺
- .......
上下文切換
上下文切換:在某一時刻,一核CPU只能執行一個進程。相當於內核只能讓一核CPU為一個機器人提供驅動力讓它動起來(1:1),多核CPU可以同時為多個機器人提供驅動力。
- 但是操作系統上有很多機器人在干活,所以內核要控制CPU不斷的為不同機器人來回提供驅動力,這是進程切換(這是站在內核的角度上看的,也叫上下文切換)
- 為了讓你感覺機器人沒有停止工作,內核控制只給每個機器人一點點的CPU時間片。這就相當於轉起來的電扇,你感覺沒有間隙,其實是有的,只是間隙時間太短,人眼難辨。現在可以腦部一下,一大堆的機器人在你面前完全不停的跳着鬼步舞....
- 因為CPU要切換給不同的機器人提供驅動力,所以每次切換之前的機器人干活到了哪里以及它的狀態得記錄下來,這是上下文保留(保護現場)
- 保護現場是必要的額外的工作,頻繁上下文切換會浪費CPU在這些額外工作上
- .........
線程
線程:一個進程內可以有多個執行線程,或者說線程是進程內部的小型進程。相當於在機器人內部根據機器人自身克隆了很多個基本完全相同的體內小機器人。
- 進程內部可以有多個線程同時執行
- 進程內的所有線程擁有的源代碼完全相同。只是有些小型機器人執行任務A,有些小型機器人執行任務B。而這些任務原本是應該被那個大機器人完成的
- 所有小型機器人活動范圍相同,即某進程內所有線程都共享地址空間。但是每個小型機器人也得有自己的一點私人空間(線程有自己的棧空間)
- 小型機器人共享很多屬性(都來源於大機器人),也有很多自己的屬性
- 線程也要切換。只是線程切換需要做的額外工作要比進程切換少的多
現在,對比一下多進程和多線程?
函數
然后是協程、例程、過程。但是在解釋這個之前,先解釋下現在更為俗知的函數,以及它們和進程、線程之間的關系。
函數:一種代碼段。用來表示一個要完成的任務單元。當然,這個任務里可能也包含了其它多個子任務。
再說程序
- 程序的主體部分是函數,包括一個程序的入口函數(main函數,有些語言(一半是動態語言)不要求你敲main函數的聲明,這些語言會在程序執行的時候默默給你加上main)以及其它一些自己編寫的函數
- 除了函數外,程序中可能還有一些全局屬性的定義、一些額外的屬於編程語言自身的額外代碼,比如說程序文件頭部可能聲明這是一個包以及要導入什么包之類的
- 函數是要被進程(或線程)執行的,機器人干什么的?就是執行函數的。程序的運行從執行入口函數main開始,然后在main中調用其它你自己編寫的函數,也就是說跳轉到其它函數上去執行一個個的任務。畫重點,函數是要被執行的,函數的執行是可以進行跳轉的。
- 那些非函數代碼(比如全局屬性的定義代碼、導包代碼),是在程序被裝載准備執行的時候完成好的工作。
先總結一下重點:進程、線程是機器人,函數是機器人要干的活。
函數怎么執行的
機器人是怎么執行函數的呢?在不使用coroutine(也就是國人翻譯后的"協程")的情況下,它的執行流程是固定的:從main函數開始,跳轉執行其它函數A,main函數被擱置等待,它必須等待跳轉后的函數A執行完返回后才能回到main函數繼續向下執行,如果函數A中還有調用函數B,那么函數A被擱置等待,它必須等待函數B執行完返回后才繼續向下執行。直到main函數也執行完,程序退出。
繼續用機器人來比喻,那就是機器人要執行一個任務,但這個任務中要求臨時去執行另一個任務,那么這個機器人必須先去執行另一個任務,並只能在執行完另一個任務的時候回到之前的那個任務繼續執行。
所以,重點是:函數A中在第X行開始跳轉調用函數B時,函數A必須等待函數B執行完返回后才能繼續從第X行處開始繼續向下。
協程、例程、過程
回到正題要解釋的東西:協程、例程、過程。
很遺憾,例程、過程、子程序,它們都是函數的不同稱呼,不同的時代、不同的編程語言稱呼的方式不一樣,都是任務單元。甚至面向對象里的方法也是函數,只不過在面向對象的面具之下,它有一些和面向對象相關的特性。
最后是協程。我猜你說的協程應該是coroutine這類東西,全稱是cooperative routine,也叫做cooperative tasks。只看英文的話,意思已經很明顯了:協同運行的子程序(子程序就是例程、函數、過程)。或者說是協同執行的任務。
在解釋coroutine之前,多插一句嘴。
coroutine被翻譯為協程,個人認為是不太合理的。在shell中有coproc的概念(ksh/bash等一些shell都支持coproc的功能),cooperative process,表示協同運行的進程,按單詞字面意思,這才應該被翻譯為協程。
協同運行?這是個什么意思。回顧下剛才解釋函數(因為是co routine,所以我下面全部用routine來替換函數的說法)的執行流程在理解協同運行是什么意思。
在正常情況下,routine跳轉運行后必須原地等待跳轉后的那個routine執行完返回才能繼續從原地向下執行。
但是,使用coroutine的時候,假設routine1和coroutine2互為coroutine,那么routine1跳轉到routine2去執行的時候,它會等待routine2才能繼續向下執行,但是不一定是等待routine2執行完,也可能是等待routine2重新跳轉回routine1(因為routine2已經產生了一些可以讓routine1繼續運行的數據,而不是讓routine1繼續在那里阻塞)。同理routine2也可能會原地等待routine1,routine1再跳回routine2。
但是這怎么行,來來回回的跳轉總得退出吧?這就是跟所寫的代碼有關系了。比如某個routine中加入一個判斷,達到某一條件時就不再跳轉,而是直接向下執行。
機器人的比喻又來了。機器人要執行一個任務,但要求臨時去執行另一個任務,現在不強制規定必須執行完另一個任務才回來執行原始任務,而是可以在執行另一個任務的時候,又臨時回來執行原始任務。
如果說不好理解,那么下面這個wiki中給的生產者消費者模型的偽代碼很容易幫助理解coroutine,這個程序不一定合理,但非常適合於理解"協同"的意義。其中q
是一個隊列,生產者coroutine會跳轉到消費者coroutine,同理消費者coroutine也一樣會跳轉到生產者coroutine。
var q := new queue
coroutine produce
loop
while q is not full
create some new items
add the items to q
yield to consume
coroutine consume
loop
while q is not empty
remove some items from q
use the items
yield to produce
這就是協同運行以及coroutine的解釋,我想應該已經不難理解了。
coroutine的優點
最后,說一下coroutine的優點:
實際上,coroutine可以認為是單線程多任務的工作方式(當然,進程中實現coroutine也是可以的),因為它在單個線程中的多個任務之間直接跳轉,而多線程是通過上下文切換來實現多任務的。換句話說,coroutine提供了並發卻不並行的功能。通過coroutine,還能實現更為"實時"的上下文任務,因為coroutine之間的跳轉切換不需要任何系統調用和可能的阻塞調用,不需要像多線程一樣為了線程之間的資源同步而使用額外的互斥鎖、信號量等。