一 Context是什么
1.1 介紹
Context,翻譯為"上下文",context包定義了Context接口類型,其接口方法定義了跨API和進程之間的執行最后期限、取消信號和其他請求范圍的值
在並發程序中,由於超時、取消操作或者一些異常情況,往往需要進行搶占操作或者中斷后續操作
context常用的使用場景:
- 一個請求對應多個goroutine之間的數據交互
- 超時控制
- 上下文控制
1.2 Context接口方法
context.Context
是一個接口,該接口定義了四個需要實現的方法。具體如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
其中:
Deadline
:是獲取設置的截止時間的意思,第一個返回值是截止時間,到了這個時間點,Context 會自動發起取消請求;第二個返回值ok==false
時表示沒有設置截止時間,如果需要取消的話,需要調用取消函數進行取消。Done
:該方法返回一個只讀的 chan,類型為 struct{},我們在 goroutine 中,如果該方法返回的 chan 可以讀取,則意味着parent context
已經發起了取消請求,我們通過 Done 方法收到這個信號后,就應該做清理操作,然后退出 goroutine,釋放資源。Err
方法返回取消的錯誤原因,因為什么 Context 被取消。Value
方法獲取該 Context 上綁定的值,是一個鍵值對,所以要通過一個 Key 才可以獲取對應的值,這個值一般是線程安全的
1.3 兩個頂級Context
context包提供兩種頂級的上下文類型,這兩個內置的上下文對象作為最頂層的partent context
,衍生出更多的子上下文對象:
func Background() Context
context.Background()返回非零的空上下文。它從不被取消,沒有值,也沒有最后期限。它通常由主函數、初始化和測試使用,並且作為傳入請求的頂級上下文。
func TODO() Context
context.TODO()返回非零的空上下文。當不清楚要使用哪個上下文或者它還不可用時(,應該使用context.TODO()。
兩者區別
本質來講兩者區別不大,其源碼實現是一樣的,只不過使用場景不同,context.Background()通常由主函數、初始化和測試使用,是頂級Context;context.TODO()通常用於主協程外的其他協程向下傳遞,分析工具可識別它在調用棧中傳播
1.4 派生Context(With系列函數)
除以上兩種頂級Context類型,context包提供四種創建可派生Context類型的函數
WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel函數返回具有新done通道的父級副本。當調用返回的cancel函數或關閉父上下文的done通道時(以先發生者為准),將關閉返回的上下文的done通道。
取消此上下文將釋放與其關聯的資源,因此代碼應在此上下文中運行的操作完成后立即調用Cancel。
示例通過context控制多個協程停止:
package main
import (
"context"
"fmt"
"time"
)
func task(ctx context.Context, s string) {
lqz:
for {
select {
case <-ctx.Done():
fmt.Println("task:我收到取消指令,我結束了")
break lqz // 結束掉 label位置的循環
default:
fmt.Println("打印一次傳入的值:", s)
time.Sleep(1 * time.Second)
}
}
}
func main() {
parent := context.Background()
ctx, cancle := context.WithCancel(parent)
go task(ctx, "lqz is Nb")
go task(ctx, "lqz is handsome")
time.Sleep(5 * time.Second) // 睡個5s鍾,發現上面兩句話不停打印
cancle() // 通過ctx控制,上面兩個go協程關閉
time.Sleep(5 * time.Second) // 睡個5s鍾,發現確實被停止了,不打印了
}
WithDeadline
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
// 注意第二個參數是時間對象
WithDeadline函數返回父上下文的副本,其截止時間調整為不遲於d。如果父上下文的截止時間早於d,則WithDeadline(Parent,d)在語義上等同於父上下文。當截止時間到期、調用返回的cancel函數或關閉父上下文的done通道(以先發生者為准)時,返回的上下文的done通道將關閉。
取消此上下文將釋放與其關聯的資源,因此代碼應在此上下文中運行的操作完成后立即調用Cancel。
官方使用示例:
這個例子傳遞一個具有任意截止時間的上下文,告訴一個阻塞函數一旦到達它就應該放棄它的工作。
package main
import (
"context"
"fmt"
"time"
)
func task(ctx context.Context, s string) {
lqz:
for {
select {
case <-ctx.Done():
fmt.Println("task:我收到取消指令,我結束了")
fmt.Println(ctx.Err())
// 正常到時間:context deadline exceeded
// 手動調用cancel :context canceled
break lqz // 結束掉 label位置的循環
default:
fmt.Println("打印一次傳入的值:", s)
time.Sleep(1 * time.Second)
}
}
}
func task2(ctx context.Context, s string) {
lqz:
for {
select {
case <-ctx.Done():
fmt.Println("task:我收到取消指令,我結束了")
fmt.Println(ctx.Err())
// 正常到時間:context deadline exceeded
// 手動調用cancel :context canceled
break lqz // 結束掉 label位置的循環
case <-time.After(1 * time.Second):
fmt.Println("1s時間到了,打印:",s)
fmt.Println(ctx.Err()) // 執行到此,如果還沒到結束時間,Err為nil
}
}
}
func main() {
// 1 正常到時間
//parent := context.Background()
//t:=time.Now().Add(5*time.Second) // 5s后的時間
//ctx, _ := context.WithDeadline(parent,t)
//go task(ctx, "lqz is Nb")
//time.Sleep(10 * time.Second) // 睡個10s鍾,由於5s結束,后5s沒有輸出
// 2 手動調用cancle取消
//parent := context.Background()
//t := time.Now().Add(5 * time.Second) // 5s后的時間
//ctx, cancel := context.WithDeadline(parent, t)
//go task(ctx, "lqz is Nb")
//time.Sleep(3 * time.Second) // 睡個3s鍾,由於5s還沒到,手動結束
//cancel()
//time.Sleep(7 * time.Second) // 再睡7s看輸出
//3 1s后輸出一次內容的另一種寫法
parent := context.Background()
t := time.Now().Add(5 * time.Second) // 5s后的時間
ctx, cancel := context.WithDeadline(parent, t)
go task2(ctx, "lqz is Nb")
time.Sleep(3 * time.Second) // 睡個3s鍾,由於5s還沒到,手動結束
cancel()
time.Sleep(7 * time.Second) // 再睡7s看輸出
}
WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// 注意第二個參數是time.Duration 時間間隔
WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。取消此上下文將釋放與其關聯的資源,因此代碼應在此上下文中運行的操作完成后立即調用取消:
這個例子傳遞一個帶有超時的上下文,告訴一個阻塞函數它應該在超時結束后放棄它的工作。
package main
import (
"context"
"fmt"
"time"
)
func task(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("task:我結束了")
// cancle函數取消會打印context canceled
// 到時間取消會打印:context deadline exceeded
fmt.Println(ctx.Err())
case <-time.After(1 * time.Second):
fmt.Println("1s時間到了")
fmt.Println(ctx.Err()) // 執行到此,如果還沒到結束時間,Err為nil
}
}
func main() {
//ctx, cancle := context.WithTimeout(context.Background(), 1*time.Second) // 打印
ctx, cancle := context.WithTimeout(context.Background(), 2*time.Second)
go task(ctx)
time.Sleep(3*time.Second)
cancle()
time.Sleep(3*time.Second)
}
WithValue
func WithValue(parent Context, key, val interface{}) Context
WithValue返回父級的副本,可為上下文設置一個鍵值對。
只對傳輸進程和API的請求范圍數據使用上下文值,而不用於向函數傳遞可選參數。
提供的鍵必須是可比較的,並且不應是字符串或任何其他內置類型,以避免使用上下文的包之間發生沖突。WithValue的用戶應該為鍵定義自己的類型。為了避免在分配給接口時進行分配,上下文鍵通常具有具體的類型結構。或者,導出的上下文鍵變量的靜態類型應該是指針或接口。
示例:
package main
import (
"context"
"fmt"
"time"
)
func task(ctx context.Context) {
fmt.Println(ctx.Value("name"))
select {
case <-ctx.Done():
fmt.Println("task:我結束了")
fmt.Println(ctx.Err())
case <-time.After(1 * time.Second):
fmt.Println("1s時間到了")
fmt.Println(ctx.Err())
}
}
func main() {
ctx, _ := context.WithTimeout(context.Background(), 1*time.Second) // 1s超時的ctx
ctx = context.WithValue(ctx, "name", "lqz")
go task(ctx)
time.Sleep(5*time.Second)
}
二 Context使用示例
2.1 控制10s后,所有協程退出
使用context
包來實現線程安全退出或超時的控制:控制10s后,所有協程退出
package main
import (
"context"
"fmt"
"strconv"
"sync"
"time"
)
func task(ctx context.Context, s string, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Println(s, "--->我結束了")
//fmt.Println(ctx.Err())
return
default:
fmt.Println(s)
time.Sleep(1 * time.Second)
}
}
}
func main() {
var wg sync.WaitGroup
ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
for i := 0; i < 10; i++ {
wg.Add(1)
s := fmt.Sprintf("我是第:%v 個任務", strconv.Itoa(i))
go task(ctx, s, &wg)
}
wg.Wait()
}
當並發體超時或main
主動停止工作者Goroutine時,每個工作者都可以安全退出。
2.2 控制某個go協程執行5次就結束
// 控制goroutine 執行5次結束
func main() {
// 定義一個運行次數變量
runCount := 0
//定義一個waitgroup,等待goroutine執行完成
var wg sync.WaitGroup
// 初始化context
parent := context.Background()
// 傳入初始化的ctx,返回ctx和cancle函數
ctx, cancle := context.WithCancel(parent)
wg.Add(1) // 增加一個任務
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("任務結束")
return
default:
fmt.Printf("任務執行了%d次\n", runCount)
runCount++
}
// 執行了5次,使用ctx的取消函數將任務取消
if runCount >= 5 {
cancle()
wg.Done() // goroutine執行完成
}
}
}()
wg.Wait() //等待所有任務完成
}
2.3 打印100個素數
Go語言是帶內存自動回收特性的,因此內存一般不會泄漏。當main
函數不再使用管道時后台Goroutine有泄漏的風險。我們可以通過context
包來避免這個問題,下面是防止內存泄露的素數篩實現:
// 返回生成自然數序列的管道: 2, 3, 4, ...
func GenerateNatural(ctx context.Context) chan int {
ch := make(chan int)
go func() {
for i := 2; ; i++ {
select {
//父協程cancel()時安全退出該子協程
case <- ctx.Done():
return
//生成的素數發送到管道
case ch <- i:
}
}
}()
return ch
}
// 管道過濾器: 刪除能被素數整除的數
func PrimeFilter(ctx context.Context, in <-chan int, prime int) chan int {
out := make(chan int)
go func() {
for {
if i := <-in; i%prime != 0 {
select {
//父協程cancel()時安全退出該子協程
case <- ctx.Done():
return
case out <- i:
}
}
}
}()
return out
}
func main() {
// 使用一個可由父協程控制子協程安全退出的Context。
ctx, cancel := context.WithCancel(context.Background())
ch := GenerateNatural(ctx) // 自然數序列: 2, 3, 4, ...
for i := 0; i < 100; i++ {
// 新出現的素數打印出來
prime := <-ch
fmt.Printf("%v: %v\n", i+1, prime)
// 基於新素數構造的過濾器
ch = PrimeFilter(ctx, ch, prime)
}
//輸出100以內符合要求的素數后安全退出所有子協程
cancel()
}
當main函數完成工作前,通過調用cancel()
來通知后台Goroutine退出,這樣就避免了Goroutine的泄漏。
三 使用Context的注意事項
- 推薦以參數的方式顯示傳遞Context
- 以Context作為參數的函數方法,應該把Context作為第一個參數。
- 給一個函數方法傳遞Context的時候,不要傳遞nil,如果不知道傳遞什么,就使用context.TODO()
- Context的Value相關方法應該傳遞請求域的必要數據,不應該用於傳遞可選參數
- Context是線程安全的,可以放心的在多個goroutine中傳遞