根據乙個 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...