Context詳解
同步和非同步機制
同步和非同步機制關注的是通訊機制
同步:呼叫端需要等待被呼叫端返回資訊才繼續執行
非同步:不用理會被呼叫端,一直執行
很顯然,Go中的goroutine通訊機制就是一種非同步機制
goroutine之間的通訊
因為goroutine一開始就不受控制了,所以我們必須要想到一種優雅的控制goroutine的方式。
全域性變數
這是一種最簡單最暴力的控制goroutine的方法,我們不斷對一個全域性變數判斷,當滿足某某條件時,讓全域性變數變為false或true,從而對goroutine進行控制
sync.WaitGroup
先介紹一下里面最重要的幾個函式
var wg sync.WaitGroup
wg.Add(n) //增加n個程序
wg.Wait() //等待所有程序結束
wg.Done() //表示此程序結束,Wait需要等待的程序數減一
一個栗子
func main() {
t:=time.Now()
var wg sync.WaitGroup
wg.Add(2)
go func() {
fmt.Println("小黃好好看")
wg.Done()
}()
go func() {
fmt.Println("小程也好看")
time.Sleep(1*time.Second)
wg.Done()
}()
wg.Wait()
fmt.Println("done!")
t1:=time.Since(t)
fmt.Println("cost time:",t1)
} //通過sync.WaitGroup控制併發
我們知道,通過sync.WaitGroup控制併發是一種非常傳統的方式了,想一想就能發現,當有很多goroutine以及goroutine巢狀的時候,sync包控制併發是非常繁瑣的一件事情。
chan+select
這是一種安全且佔用資源少的goroutine退出方式
栗子:
func main() {
stop:=make(chan bool)
go func() {
for {
select {
case <- stop:
fmt.Println("監控退出,停止...")
return
default:
fmt.Println("goroutine監控中")
time.Sleep(1*time.Second)
}
}
}()
time.Sleep(5*time.Second)
fmt.Println("使用chan通知停止監控")
stop<-true
}
因為chan是併發安全的 所以我們使用chan通知goroutine退出是一種比較優雅、安全的方式 但是還是能看出來:一旦goroutine數量過多 或者因為goroutine自身的鏈式關係 比如巢狀 衍生等等 那麼chan還是很難成功處理這種場景的
Context
上面提到的巢狀goroutine和衍生等情況是存在的,比如一個網路API介面需要一個goroutine處理部分需求,這個goroutine可能又會開啟其他的goroutine 所以我們需要一種可以跟蹤goroutine的方案 才能夠達到控制它們的目的 這就是Go官方指定的處理併發程式設計的工具:Context 中文翻譯為上下文 。
Context在通知多個goroutine或者巢狀等待goroutine退出的時候是非常方便的,於是在go1.7版本之後,官方發行了context包,用於管理協程的退出
Context介面
type Context interface{
Deadline() (deadline time.Time,ok bool) //返回當前Context被取消的時間,也就是完成工作的截止時間和是否完成任務
Done() <-chan struct{} //Done方法需要返回一個只讀的Channel 這個Channel會在當前工作完成或者上下文被取消之後關閉 多次呼叫Done方法會返回同一個Channel
Err() error //被取消返回Canceled錯誤 超時返回DeadlineExceeded錯誤
Value(key interface{}) interface{}
//給Context帶上一個鍵值對
}
context控制一個goroutine:
//使用context重寫上文
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx,cancel:=context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("監控退出,停止...")
return
default:
fmt.Println("goroutine監控中")
time.Sleep(1*time.Second)
}
}
}(ctx)
time.Sleep(5*time.Second)
fmt.Println("使用context通知停止監控")
cancel()
}
context.Background() 返回一個空的context 這個空的context一般作用於整個Context樹的根節點
Context的一些函式
context.Background() //返回一個空的Context 這個Context可以作為所有context的根節點
context.Todo() //目前沒有具體的使用場景,當我們不知道該使用什麼Context的時候,可以使用這個
上面兩個方法實現了Context的所有介面 它們的本質是
emptyCtx結構體型別 不可取消 沒有截止時間 也沒有value
context.WithCancel(parent) //建立了一個可取消的子Context 建立一個可取消的子Context 然後我們把這個子Context當作引數傳給goroutine,這樣就可以使用這個子Context跟蹤這個goroutine 然後再在goroutine中使用select <-ctx.Done()
判斷是否要結束 如果收到了這個值 就返回結束goroutine,如果接收不到,那麼就繼續進行監控
cancel() //傳送結束命令 這個函式就是WithCancel的第二個返回值
context控制多個goroutine:
func main() {
ctx,cannel:=context.WithCancel(context.Background())
go watch(ctx,"監控1")
go watch(ctx,"監控2")
go watch(ctx,"監控3")
time.Sleep(5*time.Second)
fmt.Println("使用context通知多個goroutine退出")
cannel()
}
func watch(ctx context.Context,name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name,"監控退出,停止了...")
return
default:
fmt.Println(name,"goroutine監控中...")
time.Sleep(1*time.Second)
}
}
} //使用Context通知多個goroutine退出
每個goroutine傳入的context都是context.Background()生成的同一個子context 當我們使用cancel函式通知退出 三個goroutine都i會收到訊息 這就是Context的控制能力,它就像一個控制器一樣,按下開關後,所有基於這個 Context和衍生的子Context只要是被當作引數傳給了goroutine,都可以控制goroutine的退出,不得不說這是一種優雅的處理goroutine啟動之後不可控的方法
Context的衍生繼承
有了根Context如何衍生出更多的子Context呢?這就需要用到我們的With系列函數了
func WithCancel(parent Context) (ctx Context,cancel CancelFunc)
func WithDeadline(parent Context,deadline time.Time) (Context,CancelFunc)
func WitchTimeout(parent Context,timeout time.Time) (Context,CancelFunc)
func WithValue(parent Context,key,val interface{}) Context
這四個with函式接受的都是父親Context 並且生成子Context 前三個返回值裡面都有CanceFunc(取消函式),它就像一個控制器一樣,一旦觸發,無論子Context有多少個,疊加多少層都會一次性關閉。第二個With函式和第三個With函式都是設立一個過期時間,非常類似第四個函式是給Context繫結一個鍵值對
Context使用原則
-
不要把Context放在結構體裡面,而是要當作引數傳遞
-
以Context作為引數的函式方法,應該把Context當作第一個引數
-
給一個函式方法傳遞Context的時候,如果不知道具體應該傳什麼,那麼不要傳nil,傳context.Todo
-
context的value相關方法應該傳遞必須的函式,不要什麼資料都用value盡心傳遞
-