Go 協程超時控制的實現
Go 協程超時控制
- Select 阻塞方式
- Context 方式
先說個場景:
假設業務中 A 服務需要調用 服務B,要求設置 5s 超時,那麼如何優雅實現?
Select 超時控制
考慮是否可以用 select + time.After 方式進行實現
這裡主要利用的是通道在攜程之間通信的特點,當程序調用成功後,會向通道中發送信號。沒調用成功前,通道會阻塞。
select { case res := <-c2: fmt.Println(res) case <-time.After(time.Second * 3): fmt.Println("timeout 2") }
當 c2 通道中有數據時,並且超時時間沒有達到 3s,走 case res := <-c2 這個業務邏輯,當超時時間達到 3s , 走的 case <-time.After(time.Second * 3) 這個業務邏輯, 這樣就可以實現超時 3s 的控制。
res:= <-c2 是因為channel 可以實現阻塞,那麼 time.After 為啥可以阻塞呢?
看 After 源碼。sleep.go 可以看到其實也是 channel
func After(d Duration) <-chan Time { return NewTimer(d).C }
完整代碼示例:
package timeout import ( "fmt" "testing" "time" ) func TestSelectTimeOut(t *testing.T) { // 在這個例子中, 假設我們執行瞭一個外部調用, 2秒之後將結果寫入c1 c1 := make(chan string, 1) go func() { time.Sleep(time.Second * 2) c1 <- "result 1" }() // 這裡使用select來實現超時, `res := <-c1`等待通道結果, // `<- Time.After`則在等待1秒後返回一個值, 因為select首先 // 執行那些不再阻塞的case, 所以這裡會執行超時程序, 如果 // `res := <-c1`超過1秒沒有執行的話 select { case res := <-c1: fmt.Println(res) case <-time.After(time.Second * 1): fmt.Println("timeout 1") } // 如果我們將超時時間設為3秒, 這個時候`res := <-c2`將在 // 超時case之前執行, 從而能夠輸出寫入通道c2的值 c2 := make(chan string, 1) go func() { time.Sleep(time.Second * 2) c2 <- "result 2" }() select { case res := <-c2: fmt.Println(res) case <-time.After(time.Second * 3): fmt.Println("timeout 2") } }
運行結果:
=== RUN TestSelectTimeOut
timeout 1
result 2
— PASS: TestSelectTimeOut (3.00s)
PASS
go timer 計時器
這個是 timer 類似的計時器實現,通用也是通過通道來發送數據。
package main import "time" import "fmt" func main() { // Ticker使用和Timer相似的機制, 同樣是使用一個通道來發送數據。 // 這裡我們使用range函數來遍歷通道數據, 這些數據每隔500毫秒被 // 發送一次, 這樣我們就可以接收到 ticker := time.NewTicker(time.Millisecond * 500) go func() { for t := range ticker.C { fmt.Println("Tick at", t) } }() // Ticker和Timer一樣可以被停止。 一旦Ticker停止後, 通道將不再 // 接收數據, 這裡我們將在1500毫秒之後停止 time.Sleep(time.Millisecond * 1500) ticker.Stop() fmt.Println("Ticker stopped") }
go context
context 監聽是否有 IO 操作,開始從當前連接中讀取網絡請求,每當讀取到一個請求則會將該cancelCtx傳入,用以傳遞取消信號,可發送取消信號,取消所有進行中的網絡請求。
go func(ctx context.Context, info *Info) { timeLimit := 120 timeoutCtx, cancel := context.WithTimeout(ctx, time.Duration(timeLimit)*time.Millisecond) defer func() { cancel() wg.Done() }() resp := DoHttp(timeoutCtx, info.req) }(ctx, info)
關鍵看業務代碼: resp := DoHttp(timeoutCtx, info.req) 業務代碼中包含 http 調用 NewRequestWithContext
req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(paramString))
上面的代碼,設置瞭過期時間,當DoHttp(timeoutCtx, info.req) 處理時間超過超時時間時,會自動截止,並且打印 context deadline exceeded。
看個代碼:
package main import ( "context" "fmt" "testing" "time" ) func TestTimerContext(t *testing.T) { now := time.Now() later, _ := time.ParseDuration("10s") ctx, cancel := context.WithDeadline(context.Background(), now.Add(later)) defer cancel() go Monitor(ctx) time.Sleep(20 * time.Second) } func Monitor(ctx context.Context) { select { case <-ctx.Done(): fmt.Println(ctx.Err()) case <-time.After(20 * time.Second): fmt.Println("stop monitor") } }
運行結果:
=== RUN TestTimerContext
context deadline exceeded
— PASS: TestTimerContext (20.00s)
PASS
Context 接口有如下:
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
- Deadline — 返回 context.Context 被取消的時間,也就是完成工作的截止日期;
- Done — 返回一個 Channel,這個 Channel 會在當前工作完成或者上下文被取消之後關閉,多次調用 Done 方法會返回同一個 Channel;
- Err — 返回 context.Context 結束的原因,它隻會在 Done 返回的 Channel 被關閉時才會返回非空的值;
- 如果 context.Context 被取消,會返回 Canceled 錯誤;
- 如果 context.Context 超時,會返回 DeadlineExceeded 錯誤;
- Value — 從 context.Context 中獲取鍵對應的值,對於同一個上下文來說,多次調用 Value 並傳入相同的 Key 會返回相同的結果,該方法可以用來傳遞請求特定的數據;
到此這篇關於Go 協程超時控制的實現的文章就介紹到這瞭,更多相關Go 協程超時控制內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- golang 定時任務方面time.Sleep和time.Tick的優劣對比分析
- Golang定時器Timer與Ticker的使用詳解
- Go timer如何調度
- Go語言實現超時的三種方法實例
- Go語言context上下文管理的使用