Go
語言裏每個併發的執行單元叫作goroutine
,當一個用Go
語言編寫的程序啓動時,其main
函數在一個單獨的goroutine
中運行。main
函數返回時,全部的goroutine
都會被直接打斷,程序退出。除此以外若是想經過編程的方法讓一個goroutine
中斷其餘goroutine
的執行,只能是經過在多個goroutine
間經過context
上下文對象同步取消信號的方式來實現。數據庫
這篇文章將介紹一些使用context
對象同步信號取消中斷程序執行的經常使用模式和最佳實踐,從而讓咱們能構建更迅捷、健壯的應用程序。若是對context對象不太瞭解的同窗建議先仔細看看《Golang 併發編程之Context》瞭解一下基礎。編程
簡單來講,咱們須要取消功能來防止系統作一些沒必要要的工做。瀏覽器
考慮如下常見的場景:一個HTTP
服務器查詢數據庫並將查詢到的數據做爲響應返回給客戶端:bash
若是一切正常,時序圖將以下所示: 服務器
可是,若是客戶端在中途取消了請求會發生什麼?這種狀況能夠發生在,好比用戶在請求中途關閉了瀏覽器。若是不支持取消功能,HTTP
服務器和數據庫會繼續工做,因爲客戶端已經關閉因此他們工做的成果也就被浪費了。這種狀況的時序圖以下所示:併發
理想狀況下,若是咱們知道某個處理過程(在此示例中爲HTTP請求)已中止,則但願該過程的全部下游組件都中止運行:函數
如今咱們知道了應用程序爲何須要取消功能,接下來咱們開始探究在Go
中如何實現它。由於「取消事件」與正在執行的操做高度相關,所以很天然地會將它與上下文捆綁在一塊兒。工具
取消功能須要從兩方面實現才能完成:測試
Go
語言context
標準庫的Context
類型提供了一個Done()
方法,該方法返回一個類型爲<-chan struct{}
的channel
。每次context
收到取消事件後這個channel
都會接收到一個struct{}
類型的值。因此在Go
語言裏監聽取消事件就是等待接收<-ctx.Done()
。google
舉例來講,假設一個HTTP
服務器須要花費兩秒鐘來處理一個請求。若是在處理完成以前請求被取消,咱們想讓程序能當即中斷再也不繼續執行下去:
func main() {
// 建立一個監聽8000端口的服務器
http.ListenAndServe(":8000", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 輸出到STDOUT展現處理已經開始
fmt.Fprint(os.Stdout, "processing request\n")
// 經過select監聽多個channel
select {
case <-time.After(2 * time.Second):
// 若是兩秒後接受到了一個消息後,意味請求已經處理完成
// 咱們寫入"request processed"做爲響應
w.Write([]byte("request processed"))
case <-ctx.Done():
// 若是處理完成前取消了,在STDERR中記錄請求被取消的消息
fmt.Fprint(os.Stderr, "request cancelled\n")
}
}))
}
複製代碼
你能夠經過運行服務器並在瀏覽器中打開localhost:8000
進行測試。若是你在2秒鐘前關閉瀏覽器,則應該在終端窗口上看到「request cancelled」字樣。
若是你有一個能夠取消的操做,則必須經過context
發出取消事件。能夠經過context
包的WithCancel
函數返回的取消函數來完成此操做(withCancel
還會返回一個支持取消功能的上下文對象)。該函數不接受參數也不返回任何內容,當須要取消上下文時會調用該函數,發出取消事件。
考慮有兩個相互依賴的操做的狀況。在這裏,「依賴」是指若是其中一個失敗,那麼另外一個就沒有意義,而不是第二個操做依賴第一個操做的結果(那種狀況下,兩個操做不能並行)。在這種狀況下,若是咱們很早就知道其中一個操做失敗,那麼咱們就會但願能取消全部相關的操做。
func operation1(ctx context.Context) error {
// 讓咱們假設這個操做會由於某種緣由失敗
// 咱們使用time.Sleep來模擬一個資源密集型操做
time.Sleep(100 * time.Millisecond)
return errors.New("failed")
}
func operation2(ctx context.Context) {
// 咱們使用在前面HTTP服務器例子裏使用過的類型模式
select {
case <-time.After(500 * time.Millisecond):
fmt.Println("done")
case <-ctx.Done():
fmt.Println("halted operation2")
}
}
func main() {
// 新建一個上下文
ctx := context.Background()
// 在初始上下文的基礎上建立一個有取消功能的上下文
ctx, cancel := context.WithCancel(ctx)
// 在不一樣的goroutine中運行operation2
go func() {
operation2(ctx)
}()
err := operation1(ctx)
// 若是這個操做返回錯誤,取消全部使用相同上下文的操做
if err != nil {
cancel()
}
}
複製代碼
任何須要在請求的最大持續時間內維持SLA(服務水平協議)的應用程序,都應使用基於時間的取消。該API與前面的示例幾乎相同,但有一些補充:
// 這個上下文將會在3秒後被取消
// 若是須要在到期前就取消能夠像前面的例子那樣使用cancel函數
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
// 上下文將在2009-11-10 23:00:00被取消
ctx, cancel := context.WithDeadline(ctx, time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC))
複製代碼
例如,程序在對外部服務進行HTTP API
調用時設置超時時間。若是被調用服務花費的時間太長,到時間後就會取消請求:
func main() {
// 建立一個超時時間爲100毫秒的上下文
ctx := context.Background()
ctx, _ = context.WithTimeout(ctx, 100*time.Millisecond)
// 建立一個訪問Google主頁的請求
req, _ := http.NewRequest(http.MethodGet, "http://google.com", nil)
// 將超時上下文關聯到建立的請求上
req = req.WithContext(ctx)
// 建立一個HTTP客戶端並執行請求
client := &http.Client{}
res, err := client.Do(req)
// 若是請求失敗了,記錄到STDOUT
if err != nil {
fmt.Println("Request failed:", err)
return
}
// 請求成功後打印狀態碼
fmt.Println("Response received, status code:", res.StatusCode)
}
複製代碼
根據Google
主頁響應你請求的速度,你將收到:
Response received, status code: 200
複製代碼
或者:
Request failed: Get http://google.com: context deadline exceeded
複製代碼
對於咱們來講一般都會收到第二條消息:)
儘管Go
中的上下文取消功能是一種多功能工具,可是在繼續操做以前,你須要牢記一些注意事項。其中最重要的是,上下文只能被取消一次。若是您想在同一操做中傳播多個錯誤,那麼使用上下文取消可能不是最佳選擇。使用取消上下文的場景是你實際上確實要取消某項操做,而不只僅是通知下游進程發生了錯誤。 還須要記住的另外一件事是,應該將相同的上下文實例傳遞給你可能要取消的全部函數和goroutine
。
用WithTimeout
或WithCancel
包裝一個已經支持取消功能的上下文將會形成多種可能會致使你的上下文被取消的狀況,應該避免這種二次包裝。