學會使用context取消goroutine執行的方法
Go
語言裡每一個併發的執行單元叫做goroutine
,當一個用Go
語言編寫的程式啟動時,其main
函式在一個單獨的goroutine
中執行。main
函式返回時,所有的goroutine
都會被直接打斷,程式退出。除此之外如果想通過程式設計的方法讓一個goroutine
中斷其他goroutine
的執行,只能是在多個goroutine
間通過context
上下文物件同步取消訊號的方式來實現。
這篇文章將介紹一些使用context
物件同步訊號,取消goroutine執行的常用模式和最佳實踐,從而讓我們能構建更迅捷、健壯的應用程式。如果對context物件不太瞭解的同學建議先仔細看看《Golang 併發程式設計之Context》瞭解一下基礎。
為什麼需要取消功能
簡單來說,我們需要取消功能來防止系統做一些不必要的工作。
考慮以下常見的場景:一個HTTP
伺服器查詢資料庫並將查詢到的資料作為響應返回給客戶端:
客戶端請求
如果一切正常,時序圖將如下所示:
請求處理時序圖
但是,如果客戶端在中途取消了請求會發生什麼?這種情況可以發生在,比如使用者在請求中途關閉了瀏覽器。如果不支援取消功能,HTTP
伺服器和資料庫會繼續工作,由於客戶端已經關閉所以他們工作的成果也就被浪費了。這種情況的時序圖如下所示:
不支援取消的處理時序圖
理想情況下,如果我們知道某個處理過程(在此示例中為HTTP請求)已停止,則希望該過程的所有下游元件都停止執行:
支援取消的處理時序圖
使用context實現取消功能
現在我們知道了應用程式為什麼需要取消功能,接下來我們開始探究在Go
中如何實現它。因為“取消事件”與正在執行的操作高度相關,因此很自然地會將它與上下文捆綁在一起。
取消功能需要從兩方面實現才能完成:
-
監聽取消事件
-
發出取消事件
監聽取消事件
Go
語言context
標準庫的Context
型別提供了一個Done()
方法,該方法返回一個型別為<-chan struct{}
的channel
。每次context
收到取消事件後這個channel
都會接收到一個struct{}
型別的值。所以在Go
語言裡監聽取消事件就是等待接收<-ctx.Done()
。
舉例來說,假設一個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
對於我們來說通常都會收到第二條訊息:)
context使用上的一些陷阱
儘管Go
中的上下文取消功能是一種多功能工具,但是在繼續操作之前,你需要牢記一些注意事項。其中最重要的是,上下文只能被取消一次。如果您想在同一操作中傳播多個錯誤,那麼使用上下文取消可能不是最佳選擇。使用取消上下文的場景是你實際上確實要取消某項操作,而不僅僅是通知下游程式發生了錯誤。還需要記住的另一件事是,應該將相同的上下文例項傳遞給你可能要取消的所有函式和goroutine
。
用WithTimeout
或WithCancel
包裝一個已經支援取消功能的上下文將會造成多種可能會導致你的上下文被取消的情況,應該避免這種二次包裝。
近期文章推薦
相關文章
- Go:21---goroutine併發執行體Go
- React的Context的使用方法簡介ReactContext
- Golang 使用執行命令帶管道符執行的方法Golang
- 家庭版win10怎麼取消以管理員身份執行_win10家庭版取消以管理員身份執行的方法Win10
- SpringBoot手動取消介面執行方案Spring Boot
- 使goroutine同步的方法總結Go
- Go runtime 排程器精講(四):執行 main goroutineGoAI
- 使用Runnable介面實現執行緒的方法執行緒
- 細說 Android 下的多執行緒,學會了多執行緒,你就學會了壓榨CPU!Android執行緒
- java 多執行緒之使用 interrupt 停止執行緒的幾種方法Java執行緒
- Go runtime 排程器精講(六):非 main goroutine 執行GoAI
- goroutine併發執行多個任務並依次返回結果Go
- 多執行緒(五)---執行緒的Yield方法執行緒
- [20181107]18c新特性取消執行的sql.txtSQL
- 面試官:Context攜帶資料是執行緒安全的嗎?面試Context執行緒
- MySQL記憶體執行緒獨享使用的方法MySql記憶體執行緒
- filezilla使用,4步學會filezilla使用方法
- GO 之 Goroutine 學習Go
- 在oracle中跟蹤會話執行語句的幾種方法Oracle會話
- 學會Zynq(5)GPIO中EMIO的使用方法
- 學會Zynq(4)GPIO中MIO的使用方法
- Context的典型使用場景Context
- go語言學習-goroutineGo
- 瞬間教你學會使用java中list的retainAll方法JavaAI
- 【機器學習】使用Octave執行命令機器學習
- 打算使用 docker laradock 執行 phalcon 學習Docker
- Golang中context使用GolangContext
- Java多執行緒-執行緒池的使用Java執行緒
- 執行緒、開啟執行緒的兩種方式、執行緒下的Join方法、守護執行緒執行緒
- Java多執行緒學習(六)Lock鎖的使用Java執行緒
- 我會手動建立執行緒,為什麼讓我使用執行緒池?執行緒
- RecBole小白入門系列部落格(三)——Context類模型執行流程Context模型
- Go高效併發 10 | Context:多執行緒併發控制神器GoContext執行緒
- JAVA不使用執行緒池來處理的非同步的方法Java執行緒非同步
- 建立執行緒的4種方法 and 執行緒的生命週期執行緒
- Golang語言並行設計的核心goroutineGolang並行
- 教你Python使用shutil操作檔案、subprocess執行子程式的方法Python
- java執行緒執行緒休眠,sleep方法Java執行緒