1. 為什麼需要context
在併發程式中,由於超時、取消操作或者一些異常情況,往往需要進行搶佔操作或者中斷後續操作。
舉個例子:在 Go http包的Server中,每一個請求在都有一個對應的 goroutine 去處理。請求處理函式通常會啟動額外的 goroutine 用來訪問後端服務,比如資料庫和RPC服務,用來處理一個請求的 goroutine 通常需要訪問一些與請求特定的資料,比如終端使用者的身份認證資訊、驗證相關的token、請求的截止時間。 當一個請求被取消或超時時,所有用來處理該請求的 goroutine 都應該迅速中斷退出,然後系統才能釋放這些 goroutine 佔用的資源。context深入理解可參考
context常用的使用場景:
1) 一個請求對應多個goroutine之間的資料互動
2) 超時控制
3) 上下文控制
2. context包簡介
context.Context介面:
type Context interface { // 返回Context的超時時間(超時返回場景) Deadline() (deadline time.Time, ok bool) // 在Context超時或取消時(即結束了)返回一個關閉的channel // 即如果當前Context超時或取消時,Done方法會返回一個channel,然後其他地方就可以通過判斷Done方法是否有返回(channel),如果有則說明Context已結束 // 故其可以作為廣播通知其他相關方本Context已結束,請做相關處理。 Done() <-chan struct{} // 返回Context取消的原因 Err() error // 返回Context相關資料 Value(key interface{}) interface{} }
繼承的Context,BackGound是所有Context的root,不能夠被cancel。context包提供了三種context,分別是是普通context,超時context以及帶值的context:
// 普通context,通常這樣呼叫: ctx, cancel := context.WithCancel(context.Background())
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 帶超時的context,超時之後會自動close物件的Done,與呼叫CancelFunc的效果一樣
// WithDeadline 明確地設定一個d指定的系統時鐘時間,如果超過就觸發超時
// WithTimeout 設定一個相對的超時時間,也就是deadline設為timeout加上當前的系統時間
// 因為兩者事實上都依賴於系統時鐘,所以可能存在微小的誤差,所以官方不推薦把超時間隔設定得太小
// 通常這樣呼叫:ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// 帶有值的context,沒有CancelFunc,所以它只用於值的多goroutine傳遞和共享
// 通常這樣呼叫:ctx := context.WithValue(context.Background(), "key", myValue)
func WithValue(parent Context, key, val interface{}) Context
3. 場景舉例—等待組
package main
import (
"fmt"
"sync"
"time"
)
//資料接收服務主協程同子協程同步變數
var wg sync.WaitGroup
func run(i int) {
fmt.Println("start 任務ID:", i)
time.Sleep(time.Second * 1)
wg.Done() // 每個goroutine執行完畢後就釋放等待組的計數器
}
func main() {
countThread := 2 //runtime.NumCPU()
for i := 0; i < countThread; i++ {
go run(i)
}
wg.Add(countThread) // 需要開啟的goroutine等待組的計數器
//等待所有的任務都釋放
wg.Wait()
fmt.Println("任務全部結束,退出")
}
分析:對於等待組控制多併發的情況,只有所有的goroutine都結束了才算結束,只要有一個goroutine沒有結束, 那麼就會一直等,這顯然對資源的釋放是緩慢的;
優點:使用等待組的併發控制模型,適用於好多個goroutine協同做一件事情,因為每個goroutine做的都是這件事情的一部分,只有當全部的goroutine都完成,這件事情才算完成;
缺點:需要主動的通知某一個 goroutine 結束。
疑問:如果開啟一個後臺 goroutine 一直做事情,現在不需要了,那麼就需要通知這個goroutine 結束,否則它會一直跑。
4. 場景舉例—通道+select
針對等待組場景遺留的問題,解決辦法:
1) 設定全域性變數,在通知goroutine要停止時,為全域性變數賦值,但是這樣必須保證執行緒安 全,不可避免的必須為全域性變數加鎖,顯得有失便利;
2) 使用chan + select
多路複用的方式,就會優雅許多;
package main
import (
"fmt"
"time"
)
func run(stop chan bool) {
for {
select {
case <-stop:
fmt.Println("任務1結束退出")
return
default:
fmt.Println("任務1正在執行中")
time.Sleep(time.Second * 2)
}
}
}
func main() {
stop := make(chan bool)
go run(stop) // 開啟goroutine
// 執行一段時間後停止
time.Sleep(time.Second * 10)
fmt.Println("停止任務1。。。")
stop <- true
time.Sleep(time.Second * 3)
return
}
優點:優雅、簡單
不足:如果有很多 goroutine 都需要控制結束,並且這些 goroutine 又開啟其它更多的goroutine ?
5. 場景舉例—普通context
package main
import (
"context"
"fmt"
"time"
)
func run(ctx context.Context, id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("任務%v結束退出\n", id)
return
default:
fmt.Printf("任務%v正在執行中\n", id)
time.Sleep(time.Second * 2)
}
}
}
func main() {
//管理啟動的協程
ctx, cancel := context.WithCancel(context.Background())
// 開啟多個goroutine,傳入ctx
go run(ctx, 1)
go run(ctx, 2)
// 執行一段時間後停止
time.Sleep(time.Second * 10)
fmt.Println("停止任務1")
cancel() // 使用context的cancel函式停止goroutine
// 為了檢測監控過是否停止,如果沒有監控輸出,表示停止
time.Sleep(time.Second * 3)
return
}
說明:context.Background() 返回一個空的 Context,這個空的 Context 一般用於整個 Context 樹的根節點。然後使用 context.WithCancel(parent) 函式,建立一個可取消的子 Context,然後當作引數傳給 goroutine 使用,這樣就可以使用這個子 Context 跟蹤這個 goroutine。
6. 場景舉例—Context超時
package main
import (
"context"
"fmt"
"sync"
"time"
)
func coroutine(ctx context.Context, duration time.Duration, id int, wg *sync.WaitGroup) {
for {
select {
case <-ctx.Done():
fmt.Printf("協程 %d 退出\n", id)
wg.Done()
return
case <-time.After(duration):
fmt.Printf("訊息來自協程 %d\n", id)
}
}
}
func main() {
//使用WaitGroup等待所有的goroutine執行完畢,在收到<-ctx.Done()的終止訊號後使wg中需要等待的goroutine數量減一。
// 因為context只負責取消goroutine,不負責等待goroutine執行,所以需要配合一點輔助手段
//管理啟動的協程
wg := &sync.WaitGroup{}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for i := 0; i < 3; i++ {
wg.Add(1)
go coroutine(ctx, 1*time.Second, i, wg)
}
wg.Wait()
}
說明:程式碼中使用WaitGroup等待所有的goroutine執行完畢,在收到<-ctx.Done()的終止訊號後使wg中需要等待的goroutine數量減一, 因為context只負責取消goroutine,不負責等待goroutine執行,需要配合一點輔助手段。
7. 場景舉例—Context傳遞後設資料
package main
import (
"context"
"fmt"
"time"
)
var key string = "name"
func run(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Printf("任務%v結束退出\n", ctx.Value(key))
return
default:
fmt.Printf("任務%v正在執行中\n", ctx.Value(key))
time.Sleep(time.Second * 2)
}
}
}
func main() {
//管理啟動的協程
ctx, cancel := context.WithCancel(context.Background())
// 給ctx繫結鍵值,傳遞給goroutine
valuectx := context.WithValue(ctx, key, "【監控1】")
// 開啟goroutine,傳入ctx
go run(valuectx)
// 執行一段時間後停止
time.Sleep(time.Second * 10)
fmt.Println("停止任務")
cancel() // 使用context的cancel函式停止goroutine
// 為了檢測監控過是否停止,如果沒有監控輸出,表示停止
time.Sleep(time.Second * 3)
}
8. context總結
1) 不要把 Context 放在結構體中,要以引數的方式傳遞
2) 以 Context 作為引數的函式方法,應該把 Context 作為第一個引數,放在第一位
3) 給一個函式方法傳遞 Context 的時候,不要傳遞 nil,如果不知道傳遞什麼,就使用 context.TODO
4) Context 的 Value 相關方法應該傳遞必須的資料,不要什麼資料都使用這個傳遞
5) Context 是執行緒安全的,可以放心的在多個 goroutine 中傳遞
本作品採用《CC 協議》,轉載必須註明作者和本文連結