Go 寫乙個後台協程的通用套路

2021-10-09 22:07:06 字數 4827 閱讀 4898

根據乙個 goroutine 是否直接依賴使用者互動,我們可以將 goroutine 分為兩大類,一類是直接依賴使用者互動的前台協程,比如 http server handler等;另一類是不直接依賴使用者互動的後台協程,比如 http server,定時任務協程等。前台協程隨使用者的互動開始執行,隨互動結束而結束,比較容易設計。本文主要討論後台協程設計的一些通用套路。

乙個良好的後台協程需要至少滿足以下兩個訴求:

針對這兩個訴求,我們來尋找乙個通用的實現套路。

得益於 go 從語法上對併發的支援,寫乙個簡陋的後台協程再簡單不過了。我們從下面這個 demo 開始討論,這個 demo 的任務很簡單,每隔一秒鐘將下乙個斐波那契數輸出在標準輸出裡面。

package main

type fibonacci struct

func

newfibonacci()

*fibonacci

}func

(f *fibonacci)

run()}

()}func

main()

直接執行這個程式,什麼都不會輸出,因為主協程裡面沒有任何邏輯執行,程式啟動後直接就退出了,對吧?不過現實中許多後台協程就是這樣寫的,因為真實世界裡很多主協程是有其它任務在執行的,所以 fibonacci 會一直執行下去,直到程式結束。

觀察上面這個 fibonacci 我們會發現它的一些缺陷:首先我們沒法終止它,一旦啟動就失控了;其次我們也沒法觀察它,比如在任何時候去向它要乙個當前時間的斐波那契數,是要不到的。

先說控制,我們很容易想到一種方式,就是使用乙個bool變數去維護協程是否需要繼續執行下去。

然後獲取斐波那契數這個事情也很簡單,加乙個方法就好了。

實際上,這種方案就是我遇到的大多數協程的實現方式。我們在 fibonacci 上按這個方案寫,**就是這樣:

type fibonacci struct

func

newfibonacci()

*fibonacci

}func

(f *fibonacci)

run(

) time.

sleep

(time.second)

f.mtx.

lock()

fmt.

println

(f.b)

f.a, f.b = f.b, f.a + f.b

f.mtx.

unlock()

}}()

}// 呼叫 stop 結束

func

(f *fibonacci)

stop()

func

(f *fibonacci)

isstop()

// value 獲取當前的斐波那契數

func

(f *fibonacci)

value()

int

觀察入門版的**,我們會發現一些潛在的問題。首先,新增bool變數的方法的問題是需要自己維護一把鎖,隨著程式的公升級,這把鎖有可能會被用去保護別的變數,比如在**中我們就用它來保護斐波那契數了。這樣的做法可能會帶來效能下降,如果邏輯不對甚至可能會出現死鎖問題。

另外我們繼續觀察這段**還會發現另乙個問題,即我們呼叫stop後,實際上很可能協程並不會馬上結束,它有可能正好處在 sleep 狀態,所以 stop 呼叫後,很可能過幾秒會再列印乙個數,然後協程才結束。

一般做到這一步時,會有人用想到用 channel 來代替bool變數了。我遇到的部分有經驗的工程師會用這個辦法。用 channel 有乙個好處,是可以通過對多個channel同時select監聽的方式,達到立馬生效的效果。**如下:

type fibonacci struct

mtx sync.mutex

}func

newfibonacci()

*fibonacci ),

}}func

(f *fibonacci)

run()}

}()}

// 呼叫 stop 結束

func

(f *fibonacci)

stop()

// value 獲取當前的斐波那契數

func

(f *fibonacci)

value()

int

這段**基本上就是比較常見的實現得比較好的後台協程**了,我們呼叫start(),它就執行,呼叫stop(),就立馬結束,呼叫value()就拿到結果。看上去還不錯。

我們觀察高階版的實現,似乎挑不出什麼毛病了。但實際上還有三個問題。

第乙個問題是,如果程式中有不定量的類似 fibonacci 這樣的後台協程,如何用一套簡單且行之有效的方式統一地控制它們,同時也保留單個控制的能力?

有一種簡單的想法是,在程式中宣告乙個帶stop方法inte***ce,然後用乙個slice或map儲存所有可以stop的後台協程,在需要stop的時候依次呼叫它們。

第二個問題是,在這段**中我們只是計算一下f.a+f.b並且print出來,不太會panic。在真實的**中後台協程**是有可能出現panic的,我們不光要避免這種panic由於未被recover導致整個程式崩潰,還需要在出現panic後自動恢復。

第三個問題是,如果連續呼叫stop()兩次,第二次就會因為關閉乙個已經關閉的channel而出現panic。

這些問題我們要自己解決起來也不是不行,但是如果自己解決下去的話,會寫出很多**,這不符合我對通用套路的標準:容易理解,實現成本低,不會因為過於複雜而難以在每個地方使用。

那麼有沒有簡單高效的辦法做到寫出乙個優雅的後台協程呢?辦法是有的,答案就在標準庫的 context 包裡面。

下面就是這個套路的**。

type fibonacci struct

func

newfibonacci()

*fibonacci

}func

(f *fibonacci)

run(ctx context.context)}}

()}func

(f *fibonacci)

loop

(ctx context.context)

<-

chan

error}(

)for}}

()return errch

}func

(f *fibonacci)

nextfibonacci()

// 呼叫 stop 結束

func

(f *fibonacci)

stop()

}// value 獲取當前的斐波那契數

func

(f *fibonacci)

value()

int

我們來簡單地看一下這個**的幾個關鍵點:

run 方法要求外部傳入乙個 context,這樣當外部取消這個 context 時,fibonacci 實際上也就結束了。

run 方法內部基於傳入的 context 又派生了乙個 context 出來,這樣做的目的是為 stop 方法賦值,呼叫 f.stop 的時候,實際上就是呼叫cancel方法來取消派生出來的 context。

run 並不直接執行業務邏輯,而是另起loop協程去執行,run 本身實際上是監督loop的執行,一旦loop出現panic,及時將其重啟。當然,loop協程也是通過context來控制的。

最基本的呼叫如下:

f :=

newfibonacci()

.run

(context.

background()

)// ... 執行一些其它操作

f.stop

()

我們可以建立一大堆類似 fibonacci 這樣用 context 控制的後台協程,然後很輕鬆地將他們全部結束。

ctx, cancel := context.

withcancel

(context.

background()

)for i :=

0; i <

100; i++

// ... 執行一些其它操作

// 呼叫cancel,100個後台協程全部結束

cancel

()

我們也可以用 context.withtimeout 建立帶超時的 context,讓 fibonacci 後台只執行一小段時間。

ctx, cancel := context.

withtimeout(25

*time.second)

for i :=

0; i <

100; i++

<-ctx.

done()

cancel

()

最重要的是,得益於 context 在標準庫中的廣泛支援,我們可以很容易地將 fibonacci 這種實現與各種控制方法結合起來,例如與 http request 結合,當乙個請求進來時啟動乙個 fibonacci,並且在請求結束後自動結束。

我們討論了寫後台協程的乙個通用套路,在這個套路裡面有兩個核心點需要遵循。

第一點是後台協程通過監聽 context 而不是自己建立的某個變數去做啟停控制,這個 context 有兩個要點:從外部傳入,在內部派生。

第二點是後台協程應該考慮實現類似 supervisor 這樣的自動重啟機制,在任務結束時自動恢復。

聊一聊go的協程

最近在學習go語言,學習到了協程,來記錄下學習的心路歷程 先來看下例子 列印5個hello和5個world package main func say s string func main go 啟動協程的方式就是使用關鍵字 go,後面一般接乙個函式或者匿名函式 執行上述 發現什麼也沒有輸出 為什麼...

程序,執行緒,協程的乙個簡單解釋

我們都知道計算機的核心是cpu,它承擔了所有計算機的任務,它就像乙個工廠,時刻執行著。假定工廠的電力有限,一次只能供給乙個車間使用,也就是說,乙個車間開工的時候,其他車間都必須停工,背後的含義就是,單個cpu一次只能執行乙個任務。程序就好比工廠裡的車間,他代表cpu所能處理的單個任務。任意時刻,cp...

python協程初步 乙個生成器的實現

和列表那種一下佔據長度為n的記憶體空間不同的是,生成器在呼叫的過程中逐步佔據記憶體空間,因此有著很大的優勢 def myfibbo num a,b 0,1 count 0 while counta,b a b,a print b count 1 執行 myfibbo 10 def myfibbo n...