聊一聊python和golang的協程區別

背景

最近在作後端服務python到go的遷移和重構,這兩種語言裏,最大的特點和優點就是都支持協程。以前一直作python的性能優化和架構優化,一開始以爲兩個協程原理和應用應該差很少,後來發現仍是有很大的區別,今天就在這裏總結一下~python

不少人學習python,不知道從何學起。git

不少人學習python,掌握了基本語法事後,不知道在哪裏尋找案例上手。github

不少已經作案例的人,殊不知道如何去學習更加高深的知識。golang

那麼針對這三類人,我給你們提供一個好的學習平臺,免費領取視頻教程,電子書籍,以及課程的源代碼!??¤web

QQ羣:1057034340vim

什麼是協程

在說它們二者區別前,咱們首先聊一下什麼是協程,好像它沒有一個官方的定義,那就結合平時的應用經驗和學習內容來談談本身的理解。後端

協程,其實能夠理解爲一種特殊的程序調用。特殊的是在執行過程當中,在子程序(或者說函數)內部可中斷,而後轉而執行別的子程序,在適當的時候再返回來接着執行。api

注意,它有兩個特徵:安全

可中斷 ,這裏的中斷不是普通的函數調用,而是相似CPU的中斷,CPU在這裏直接釋放轉到其餘程序斷點繼續執行。性能優化

可恢復 ,等到合適的時候,能夠恢復到中斷的地方繼續執行,至於什麼是合適的時候,咱們後面再探討。

和進程線程的區別

上面兩個特色就致使了它相對於線程和進程切換來講極高的執行效率,爲何這麼說呢?咱們先老生常談地說一下進程和線程。

進程是操做系統資源分配的基本單位,線程是操做系統調度和執行的最小單位。這兩句應該是咱們最常聽到的兩句話,拆開來講,進程是程序的啓動實例,擁有代碼和打開的文件資源、數據資源、獨立的內存空間。線程從屬於進程,是程序的實際執行者,一個進程至少包含一個主線程,也能夠有更多的子線程,線程擁有本身的棧空間。不管是進程仍是線程,都是由操做系統所管理和切換的。

咱們再來看協程,它又叫作微 線程,但其實它和進程還有線程徹底不是一個維度上的概念 。進程和線程的切換徹底是用戶無感,由操做系統控制,從用戶態到內核態再到用戶態。而協程的切換徹底是程序代碼控制的,在用戶態的切換,就像函數回調的消耗同樣,在線程的棧內便可完成。

python的協程(Goroutine)

python的協程實際上是咱們一般意義上的協程Goroutine。
    從概念上來說,python的協程一樣是在適當的時候可中斷可恢復。那麼什麼是適當的時候呢,就是你認爲適當的時候,由於程序在哪裏發生協程切換徹底控制在開發者手裏。固然,對於python來講,因爲GIL鎖,在CPU密集的代碼上作協程切換是沒啥意義的,CPU原本就在忙着沒偷懶,切換到其餘協程,也只是在單核內換個地方忙而已。很明顯,咱們應該在IO密集的地方來起協程,這樣可讓CPU再也不空等轉而去別的地方幹活,才能真正發揮協程的威力。 從實現上來說,若是熟知了python生成器,還能夠將協程理解爲**生成器+調度策略**,生成器中的**yield**關鍵字,就可讓生成器函數發生中斷,而調度策略,能夠驅動着協程的執行和恢復。這樣就實現了協程的概念。這裏的調度策略可能有不少種,簡單的例如忙輪循:while True,更簡單的甚至是一個for循環。就能夠驅動生成器的運行,由於生成器自己也是可迭代的。複雜的好比多是基於epool的事件循環,在python2的tornado中,以及python3的asyncio中,都對協程的用法作了更好的封裝,經過yield和await就可使用協程,經過事件循環監控文件描述符狀態來驅動協程恢復執行。

咱們看一個簡單的協程:

import time

def consumer(): r = '' while True: n = yield r if not n: return print('[CONSUMER] Consuming %s...' % n) time.sleep(1) r = '200 OK' def produce(c): c.next() n = 0 while n < 5: n = n + 1 print('[PRODUCER] Producing %s...' % n) r = c.send(n) print('[PRODUCER] Consumer return: %s' % r) c.close() if __name__=='__main__': c = consumer() produce(c)
很明顯這是一個傳統的生產者-消費者模型,這裏consumer函數就是一個協程(生成器),它在n = yield r 的地方發生中斷,生產者produce中的c.send(n),能夠驅動協程的恢復,而且向協程函數傳遞數據n,接收返回結果r。 而while n < 5,就是咱們所說的調度策略。 在生產中,這種模式很適合咱們來作一些pipeline數據的消費,咱們不須要寫死幾個生產者進程幾個消費者進程,而是用這種協程的方式,來實現CPU動態地分配調度。 若是你看過上篇文章的話,是否是發現這個golang中流水線模型有點像呢,也是生產者和消費者間進行通訊,但go是經過channel這種安全的數據結構,爲何python不須要呢,由於python的協程是在單線程內切換自己就是安全的,換句話說,協程間自己就是串行執行的。而golang則否則。思考一個有意思的問題,若是咱們將go流水線模型中channel設置爲無緩衝區時,生產者絕對驅動消費者的執行,是否是就跟python很像了呢。因此python的協程從某種意義來講,是否是golang協程的一種特殊狀況呢? 後端在線服務中咱們更經常使用的python協程實際上是在異步IO框架中使用,以前咱們也提過python協程在IO密集的系統中使用才能發揮它的威力。而且大多數的數據中間件都已經提供支持了異步包的支持,裏順便貼一個[python3支持協程的異步IO庫](https://github.com/aio-libs?page=1),基本支持了常見的異步數據中間件。 好比看一個咱們業務代碼中的片斷,asyncio支持的原生協程:

asyncio支持的基於epool的事件循環:

def main(): define_options() options.parse_command_line() # 使用uvloop代替原生事件循環 # asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) app = tornado.web.Application(handlers=handlers, debug=options.debug) http_server = tornado.httpserver.HTTPServer(app) http_server.listen(options.port) asyncio.get_event_loop().run_forever()

async/await支持的原生協程:

class RcOutputHandler(BaseHandler): async def post(self): status, msg, user = self.check_args('uid', 'order_no', 'mid', 'phone', 'name', 'apply_id', 'product_id') if status != ErrorCodeConfig.SUCCESS: status, msg, report = status, msg, None else: rcoutput_flow_instance = ZHANRONG_CUSTOM_PRODUCTID_RCFLOW_MAP.get(user.product_id, RcOutputFlowControler()) status, msg, report = await rcoutput_flow_instance.get_rcoutput_result(user) res = self.generate_response_data(status, msg, report) await self.finish(res) # 陪跑流程 await AccompanyRunningFlowControler().get_accompany_data(user)

總結一下python協程的特色:

單線程內切換,適用於IO密集型程序中,能夠最大化IO多路複用的效果。

沒法利用多核。

協程間徹底同步,不會並行。不須要考慮數據安全。

用法多樣,能夠用在web服務中,也可用在pipeline數據/任務消費中

golang的協程(goroutine)

golang的協程就和傳統意義上的協程不大同樣了,兼具協程和線程的優點。這也是go最大的特點,就是從語言層面支持併發。Go語言裏,啓動一個goroutine很容易:go function 就行。 一樣從概念上來說,golang的協程一樣是在適當的時候可中斷可恢復。當協程中發生channel讀寫的阻塞或者系統調用時,就會切換到其餘協程。具體的代碼示例能夠看上篇文章,就再也不贅述了。 從實現上來講,goroutine能夠在多核上運行,從而實現協程並行,咱們先直接看下go的調度模型MPG。

如上圖, M 指的是 Machine ,一個 M 直接關聯了一個內核線程。由操做系統管理。

P 指的是」processor」,表明了 M 所需的上下文環境,也是處理用戶級代碼邏輯的處理器。它負責銜接M和G的調度上下文,將等待執行的G與M對接。

G 指的是 Goroutine ,其實本質上也是一種輕量級的線程。包括了調用棧,重要的調度信息,例如channel等。

每次go調用的時候,都會:

  1. 建立一個G對象,加入到本地隊列或者全局隊列
  2. 若是還有空閒的P,則建立一個M
  3. M會啓動一個底層線程,循環執行能找到的G任務
  4. G任務的執行順序是,先從本地隊列找,本地沒有則從全局隊列找(一次性轉移(全局G個數/P個數)個,再去其它P中找(一次性轉移一半),
  5. 以上的G任務執行是按照隊列順序(也就是go調用的順序)執行的。

對於上面的第2-3步,建立一個M,其過程:

  1. 先找到一個空閒的P,若是沒有則直接返回,(哈哈,這個地方就保證了進程不會佔用超過本身設定的cpu個數)
  2. 調用系統api建立線程,不一樣的操做系統,調用不同,其實就是和c語言建立過程是一致的
  3. 而後建立的這個線程裏面纔是真正作事的,循環執行G任務

當協程發生阻塞切換時:

  1. M0出讓P
  2. 建立M1接管P及其任務隊列繼續執行其餘G。
  3. 當阻塞結束後,M0會嘗試獲取空閒的P,失敗的話,就把當前的G放到全局隊列的隊尾。

這裏咱們須要注意三點:

一、M與P的數量沒有絕對關係,一個M阻塞,P就會去建立或者切換另外一個M,因此,即便P的默認數量是1,也有可能會建立不少個M出來。

二、P什麼時候建立:在肯定了P的最大數量n後,運行時系統會根據這個數量建立n個P。

三、M什麼時候建立:沒有足夠的M來關聯P並運行其中的可運行的G。好比全部的M此時都阻塞住了,而P中還有不少就緒任務,就會去尋找空閒的M,而沒有空閒的,就會去建立新的M。

總結一下go協程的特色:

協程間須要保證數據安全,好比經過channel或鎖。

能夠利用多核並行執行。

協程間不徹底同步,能夠並行運行,具體要看channel的設計。

搶佔式調度,可能沒法實現公平。

coroutine(python)和goroutine(go)的區別

除了python,C#, Lua語言都支持 coroutine 特性。coroutine 與 goroutine 在名字上相似,均可以可中斷可恢復的協程,它們之間最大的不一樣是,goroutine 可能在多核上發生並行執行,單但 coroutine 始終是順序執行。也基於此,咱們應該清楚coroutine適用於IO密集程序中,而goroutine在 IO密集和CPU密集中都有很好的表現。不過話說回來,go就必定比python快麼,假如在徹底IO併發密集的程序中,python的表現反而更好,由於單線程內的協程切換效率更高。

從運行機制上來講,coroutine 的運行機制屬於協做式任務處理, 程序須要主動交出控制權,宿主才能得到控制權並將控制權交給其餘 coroutine。若是開發者無心間或者故意讓應用程序長時間佔用 CPU,操做系統也無能爲力,表現出來的效果就是計算機很容易失去響應或者死機。goroutine 屬於搶佔式任務處理,已經和現有的多線程和多進程任務處理很是相似, 雖然沒法控制本身獲取高優先度支持。但若是發現一個應用程序長時間大量地佔用 CPU,那麼用戶有權終止這個任務。

從協程和線程的對應方式來看:

N:1,Python協程模式,多個協程在一個線程中切換。在IO密集時切換效率高,但沒有用到多核

1:1,Java多線程模式,每一個協程只在一個線程中運行,這樣協程和線程沒區別,雖然用了多核,可是線程切換開銷大。

1:1,go模式,多個協程在多個線程上切換,既能夠用到多核,又能夠減小切換開銷。(當都是cpu密集時,在多核上切換好,當都是io密集時,在單核上切換好)。

從協程通訊和調度機制來看: