淺談協程

在程序遇到性能瓶頸的時候,解決方案之一就是採用併發編程技術。程序員

尤爲是使用Python這種「執行低效」的編程語言,如何用其實現高效地併發能力被屢屢提起。因爲衆所周知的緣由,在別的語言中經常使用的多線程併發編程模型在Python裏不那麼好了。編程

有羣不明就裏的閒蛋(閒的蛋疼的人),認爲GIL讓多線程沒法並行執行,他的生命的天空就是灰濛濛的。多進程太耗系統資源,多線程又不讓好好玩,因而,用Python的同窗在稍有編程經驗後,就會嘗試去弄明白Python菜鳥老鳥都常掛在嘴邊的協程是什麼。安全

相關概念

通過與無數不明就裏就想用代碼幹翻全世界的人打交道,發現他們每每不尊重、不重視基本常識。(本文對基本常識的解釋也許可能存在有誤解的地方,歡迎在公衆號後臺留言批評指正。)服務器

進程(Process)

在面向進程設計的操做系統中(如Linux 2.4及其以前),進程是程序運行的基本單位;而在面向線程設計的系統中(如Linux 2.6以後),進程只是線程的容器,操做系統調度任務的單位是線程,進程只是用於隔離不一樣的進程,不一樣進程間資源不共享,進程內能夠資源共享。數據結構

操做系統使用進程模型,是爲了實現多任務操做系統。例如,讓用戶能夠邊聽音樂邊玩網遊。多進程多是真正的利用多核CPU並行執行,也可能只是分時複用。進程的基本模型和基本行爲,都是由操做系統定義的,編程語言只能遵守實現。多線程

有人可能不解,爲啥Erlang裏的進程,就和操做系統定義的不同呢?由於不管是進程、線程,最終都是須要用代碼來編寫和實現的,接口的定義權雖然在操做系統那裏,而編程語言卻控制着具體實現。Erlang雖然有本身的「進程」模型,但只是Erlang單方面的設計把它叫作「Process」,和操做系統定義的進程是兩碼事。它其實是使用操做系統的線程接口實現的,故而實質是線程。併發

子進程、父進程、主進程

能夠從一個進程中啓動別的進程,新啓動的稱爲子進程,啓動子進程的進程稱爲父進程,原始最早執行的進程稱爲主進程。編程語言

線程(Thread)

線程是現代操做系統調度任務的基本單位,是進程的組成部分。同一進程下的多個不一樣線程,能夠共享該進程擁有的計算機資源,各個線程之間所擁有的資源通常不共享。函數

操做系統使用線程模型,是爲了提供任務分解爲多個子任務併發或並行運行的解決方案,以提升程序執行效率。例如,網遊程序既要與服務器傳輸信息,也要採集用戶的鍵盤鼠標操做,還要渲染遊戲畫面,均可以拆解爲獨立子任務分派到不一樣線程去執行。多線程能夠真正利用多核CPU並行執行,亦可分時複用。線程的基本模型和基本行爲,也是由操做系統定義的,編程語言只能遵守實現。性能

有人可能又不解,爲啥操做系統說線程能夠並行執行,而Python裏的線程卻不能並行呢?剛提到具體的實現權力在編程語言。Python爲了下降麻瓜們編寫併發程序的難度,引入了GIL鎖的概念,讓一個進程內的多線程,只能利用單核,多線程只能分時複用單核輪流執行。這種方式,有好有壞,好處是下降併發編程難度,大大減小併發編程的反作用,壞處是不能利用多核優點。

子線程、父線程、主線程

與進程的父子關係相似。能夠從一個線程中啓動別的線程,被啓動的爲子線程,原來的是父線程,原始最早執行的爲主線程。

例程(Routine)

語言級別內定義的可被調用的代碼段,爲了完成某個特定功能而封裝在一塊兒的一系列指令。通常的編程語言都用稱爲函數方法的代碼結構來體現。

子例程(Subroutine)

例程中定義的例程。注意,因爲例程能夠嵌套定義,並且例程也本就是代碼拆分設計的子程序,因此,子程序、子例程、例程等概念常常相互混用。在英文技術文章裏,routine和subroutine這兩個詞幾乎不做區分,在討論嵌套和被嵌套這種對比之下才有區分。

併發(Concurrent)


併發描述的是程序的組織結構。指程序要被設計成多個可獨立執行的子任務(以利用有限的計算機資源使多個任務能夠被實時或近實時執行爲目的)。

例如玩網遊,須要將客戶端服務端數據交換的任務和圖形繪製的任務拆分爲獨立子任務,要讓交換一下數據就當即繪製一下圖造成爲可能,在單核CPU上也能夠有較好的遊戲體驗。若是不拆分,實現這種效果將會變得很是困難。

並行(Parallel)


並行描述的是程序的執行狀態。指多個任務同時在多個CPU上執行(以利用富餘計算機資源加速完成多個任務爲目的)。則稱這些任務是並行執行的。這樣的程序稱爲能夠並行執行的程序。

如上述玩網遊例子,假如在四核機器上執行,和服務器交互的任務單獨享用一個核,圖形繪製任務再拆分爲3個獨立子任務分別單獨享用一個核,這樣不管是與服務器交互,仍是圖形的計算和渲染都會更快更流暢。

併發提供了一種程序組織結構方式,讓問題的解決方案能夠並行執行,但並行執行不是必須的

協程(Co-routine)

見名知義,協做式的例程。下面深刻解析。

協程是非搶佔式的多任務子例程的歸納,能夠容許有多個入口點在例程中肯定的位置來控制程序的暫停與恢復執行。多個入口點是指能夠在一個協程內屢次使用如yield的關鍵字,每一個yield的位置,都是程序員可使之讓出執行權、暫停、恢復、傳遞信號、注入執行結果等操做。

高德納說,例程是協程的特例。暫不深刻解析這句話,但咱們應該知道了,協程、例程本質上是一回事,不過表現有所差別。例程,就是函數、方法,因此協程在代碼的體現,也就是按照函數、方法那樣去定義的。

函數在線程內執行,協程固然也在線程內執行,多個協程共享着該線程擁有的資源。因爲協程就是函數或方法,在線程運行初始化時,因此,與函數同樣,協程的數據結構存放在線程的棧內存中。因此協程的切換,實際上仍是函數的調用,是在線程的棧內存中完成的。 進程和線程的切換,要保存的狀態很複雜不少,內存佔用量也要大不少,涉及的操做系統調度也複雜不少。這就是協程的切換開銷比線程和進程都小太多的緣由。

注意,協程是能夠跨線程調度的,就像一個函數能夠放到另外一個線程去執行同樣。

協程和進程或線程的不一樣之處。協程要有多少個入口點(即yield多少次)、和接下來調用哪一個協程(即yield誰)、各自運行什麼任務(佔用多少資源)都是程序猿在編程中實現的。這既是優勢也是缺點,可見,要用協程寫出高質量的併發代碼,對程序員的質量有很高的要求。

進程和線程都是由操做系統來調度的,何時中斷、何時返回、接下來調度誰,都是操做系統包辦。並且都是搶佔式調度,優先級平等的多進程和多線程的執行順序是不可預測的,而協程的執行順序是能夠被安排的

協程和通常例程(函數/方法)的區別。函數執行是從其第一行開始,一直到返回爲止(沒有顯式return語句的也有返回)。從開始到返回,執行完了,就退出了,生命週期隨之結束。函數在各次調用之間,並不會保存以前的執行狀態。而協程,有多個入口點,可能會被調度屢次,一個協程的暫時退出,是靠調用別的協程實現的。協程的調用,還會保存以前的執行狀態,切換到另外一個協程後,能夠再回來繼續往下執行。協程執行的起點,是進入該協程的入口點,不必定是協程定義的第一行代碼,該次調用的終點,也不必定是協程的最後一行代碼。

協程是用戶態線程嗎?

你特麼在逗我。這種說法,是在無故增長人們理解協程的負擔。協程,不是用戶態線程,也不是用戶(程序員)控制着的線程(若是換成「相似線程的東西」,勉強過得去)。

那再囉嗦一下什麼是用戶態,以及用戶態線程。

操做系統在執行代碼的時候,會對代碼區別對待,使代碼具備不一樣的權限,意味着不一樣的代碼段能夠操控不一樣的內存區域和各類計算機資源的訪問。就是爲了實現耳熟能詳的「保護模式」。保護模式是爲了不程序員提供給系統執行的代碼影響到系統的穩定性和安全性。

因此,操做系統內核的代碼幾乎都是有特權的,所謂的內核態,Ring0級特權;而程序員編寫的應用程序,大可能是面向通常用戶,幾乎都是沒有特權的,所謂用戶態,Ring3普通權限。因此,一段代碼是什麼等級,就說這段代碼就正處於該等級對應的狀態。

然而,程序員也可能須要操做系統底層資源,好比要在系統內植入病毒、木馬。當某病毒線程經過中斷門、調用門等方式進入內核態破壞操做系統的時候,它當時是內核態線程;若是這個病毒它一下子又要爲你下載日本電影作單純地文件訪問,那它當時就是用戶態線程。

明白了嗎?別再說協程是用戶態線程,這樣顯得讀的書少。

相關文章
相關標籤/搜索