python 協程與go協程的區別

從零開始的程式設計師生活發表於2019-05-09

程式、執行緒和協程

程式的定義:

程式,是計算機中已執行程式的實體。程式本身只是指令、資料及其組織形式的描述,程式才是程式的真正執行例項。

執行緒的定義:

作業系統能夠進行運算排程的最小單位。它被包含在程式之中,是程式中的實際運作單位。

程式和執行緒的關係:

一條執行緒指的是程式中一個單一順序的控制流,一個程式中可以併發多個執行緒,每條執行緒並行執行不同的任務。
CPU的最小排程單元是執行緒不是程式,所以單程式多執行緒也可以利用多核CPU.

協程的定義:

協程通過線上程中實現排程,避免了陷入核心級別的上下文切換造成的效能損失,進而突破了執行緒在IO上的效能瓶頸。

協程和執行緒的關係

協程是在語言層面實現對執行緒的排程,避免了核心級別的上下文消耗。

python協程與排程

Python的協程源於yield指令。yield有兩個功能:

  • yield item用於產出一個值,反饋給next()的呼叫方。
  • 作出讓步,暫停執行生成器,讓呼叫方繼續工作,直到需要使用另一個值時再呼叫next()。
import asyncio
import datetime

async def display_date():
    loop = asyncio.get_running_loop()
    end_time = loop.time() + 5.0
    while True:
        print(datetime.datetime.now())
        if (loop.time() + 1.0) >= end_time:
            break
        await asyncio.sleep(1)

asyncio.run(display_date())

協程是對執行緒的排程,yield類似惰性求值方式可以視為一種流程控制工具,
實現協作式多工,在Python3.5正式引入了 Async/Await表示式,使得協程正式在語言層面得到支援和優化,大大簡化之前的yield寫法。
執行緒是核心進行搶佔式的排程的,這樣就確保了每個執行緒都有執行的機會。
而 coroutine 執行在同一個執行緒中,由語言的執行時中的 EventLoop(事件迴圈)來進行排程。
和大多數語言一樣,在 Python 中,協程的排程是非搶佔式的,也就是說一個協程必須主動讓出執行機會,其他協程才有機會執行。
讓出執行的關鍵字就是 await。也就是說一個協程如果阻塞了,持續不讓出 CPU,那麼整個執行緒就卡住了,沒有任何併發。

PS: 作為服務端,event loop最核心的就是IO多路複用技術,所有來自客戶端的請求都由IO多路複用函式來處理;作為客戶端,
event loop的核心在於利用Future物件延遲執行,並使用send函式激發協程,掛起,等待服務端處理完成返回後再呼叫CallBack函式繼續下面的流程

Go的協程是天生在語言層面支援,和Python類似都是採用了關鍵字,而Go語言使用了go這個關鍵字,可能是想表明協程是Go語言中最重要的特性。

go協程之間的通訊,Go採用了channel關鍵字。

Go實現了兩種併發形式:

  • 多執行緒共享記憶體。如Java或者C++等在多執行緒中共享資料(例如陣列、Map、或者某個結構體或物件)的時候,通過鎖來訪問.
  • Go語言特有的,也是Go語言推薦的:CSP(communicating sequential processes)併發模型。

Go的CSP併發模型實現:M, P, G : [https://www.cnblogs.com/sunsky303/p/9115530.html]

package main

import (
    "fmt"
)

//Go 協程(goroutines)和協程(coroutines)
//Go 協程意味著並行(或者可以以並行的方式部署),協程一般來說不是這樣的
//Go 協程通過通道來通訊;協程通過讓出和恢復操作來通訊

// 程式退出時不會等待併發任務結束,可用通道(channel)阻塞,然後發出退出訊號
func main() {
    jobs := make(chan int)
    done := make(chan bool) // 結束標誌

    go func() {
        for {
            j, more := <-jobs //  利用more這個值來判斷通道是否關閉,如果關閉了,那麼more的值為false,並且通知給通道done
            fmt.Println("----->:", j, more)
            if more {
                fmt.Println("received job", j)
            } else {
                fmt.Println("end received jobs")
                done <- true
                return
            }
        }
    }()

    go func() {
        for j := 1; j <= 3; j++ {
            jobs <- j
            fmt.Println("sent job", j)
        }
        close(jobs) // 寫完最後的資料,緊接著就close掉
        fmt.Println("close(jobs)")
    }()

    fmt.Println("sent all jobs")
    <-done // 讓main等待全部協程完成工作
}

通過在函式呼叫前使用關鍵字 go,我們即可讓該函式以 goroutine 方式執行。goroutine 是一種 比執行緒更加輕盈、更省資源的協程。
Go 語言通過系統的執行緒來多路派遣這些函式的執行,使得 每個用 go 關鍵字執行的函式可以執行成為一個單位協程。
當一個協程阻塞的時候,排程器就會自 動把其他協程安排到另外的執行緒中去執行,從而實現了程式無等待並行化執行。
而且排程的開銷非常小,一顆 CPU 排程的規模不下於每秒百萬次,這使得我們能夠建立大量的 goroutine,
從而可以很輕鬆地編寫高併發程式,達到我們想要的目的。 ---- 某書

協程的4種狀態

  • Pending
  • Running
  • Done
  • Cacelled

和系統執行緒之間的對映關係

go的協程本質上還是系統的執行緒呼叫,而Python中的協程是eventloop模型實現,所以雖然都叫協程,但並不是一個東西.
Python 中的協程是嚴格的 1:N 關係,也就是一個執行緒對應了多個協程。雖然可以實現非同步I/O,但是不能有效利用多核(GIL)。
而 Go 中是 M:N 的關係,也就是 N 個協程會對映分配到 M 個執行緒上,這樣帶來了兩點好處:

  • 多個執行緒能分配到不同核心上,CPU 密集的應用使用 goroutine 也會獲得加速.
  • 即使有少量阻塞的操作,也只會阻塞某個 worker 執行緒,而不會把整個程式阻塞。

PS: Go中很少提及執行緒或程式,也就是因為上面的原因.

兩種協程對比:

  • async是非搶佔式的,一旦開始採用 async 函式,那麼你整個程式都必須是 async 的,不然總會有阻塞的地方(一遇阻塞對於沒有實現非同步特性的庫就無法主動讓排程器排程其他協程了),也就是說 async 具有傳染性。
  • Python 整個非同步程式設計生態的問題,之前標準庫和各種第三方庫的阻塞性函式都不能用了,requests 不能用了,redis.py 不能用了,甚至 open 函式都不能用了。所以 Python 協程的最大問題不是不好用,而是生態環境不好。
  • goroutine 是 go 與生俱來的特性,所以幾乎所有庫都是可以直接用的,避免了 Python 中需要把所有庫重寫一遍的問題。
  • Goroutine 中不需要顯式使用 await 交出控制權,但是 Go 也不會嚴格按照時間片去排程 goroutine,而是會在可能阻塞的地方插入排程。Goroutine 的排程可以看做是半搶佔式的。

PS: python非同步庫列表 [https://github.com/timofurrer/awesome-asyncio]


Do not communicate by sharing memory; instead, share memory by communicating.(不要以共享記憶體的方式來通訊,相反,要通過通訊來共享記憶體) -- CSP併發模型


擴充套件與總結

erlang和golang都是採用了CSP(Communicating Sequential Processes)模式(Python中的協程是eventloop模型)
但是erlang是基於程式的訊息通訊,go是基於goroutine和channel的通訊。
Python和Go都引入了訊息排程系統模型,來避免鎖的影響和程式/執行緒開銷大的問題。
協程從本質上來說是一種使用者態的執行緒,不需要系統來執行搶佔式排程,而是在語言層面實現執行緒的排程。
因為協程不再使用共享記憶體/資料,而是使用通訊來共享記憶體/鎖,因為在一個超級大系統裡具有無數的鎖,
共享變數等等會使得整個系統變得無比的臃腫,而通過訊息機制來交流,可以使得每個併發的單元都成為一個獨立的個體,
擁有自己的變數,單元之間變數並不共享,對於單元的輸入輸出只有訊息。
開發者只需要關心在一個併發單元的輸入與輸出的影響,而不需要再考慮類似於修改共享記憶體/資料對其它程式的影響。

相關文章