【Go進階—并發(fā)編程】Context
作者:與昊
來源:SegmentFault 思否社區(qū)
Context 是 Go 應用開發(fā)常用的并發(fā)控制技術(shù),它與 WaitGroup 最大的不同點是 Context 對于派生 goroutine 有更強的控制力,它可以控制多級的 goroutine。
盡管有很多的爭議,但是在很多場景下使用 Context 都很方便,所以現(xiàn)在它已經(jīng)在 Go 生態(tài)圈中傳播開來了,包括很多的 Web 應用框架,都切換成了標準庫的 Context。標準庫中的 database/sql、os/exec、net、net/http 等包中都使用到了 Context。而且,如果遇到了下面的一些場景,也可以考慮使用 Context:
上下文信息傳遞 ,比如處理 http 請求、在請求處理鏈路上傳遞信息;
控制子 goroutine 的運行;
超時控制的方法調(diào)用;
可以取消的方法調(diào)用。
實現(xiàn)原理
接口定義
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
context.Background():返回一個非 nil 的、空的 Context,沒有任何值,不會被 cancel,不會超時,沒有截止日期。一般用在主函數(shù)、初始化、測試以及創(chuàng)建根 Context 的時候。
context.TODO():返回一個非 nil 的、空的 Context,沒有任何值,不會被 cancel,不會超時,沒有截止日期。當你不清楚是否該用 Context,或者目前還不知道要傳遞一些什么上下文信息的時候,就可以使用這個方法。
一般函數(shù)使用 Context 的時候,會把這個參數(shù)放在第一個參數(shù)的位置。
從來不把 nil 當做 Context 類型的參數(shù)值,可以使用 context.Background() 創(chuàng)建一個空的上下文對象,也不要使用 nil。
Context 只用來臨時做函數(shù)之間的上下文透傳,不能持久化 Context 或者把 Context 長久保存。把 Context 持久化到數(shù)據(jù)庫、本地文件或者全局變量、緩存中都是錯誤的用法。
key 的類型不推薦字符串類型或者其它內(nèi)建類型,否則容易在包之間使用 Context 時候產(chǎn)生沖突。使用 WithValue 時,key 的類型應該是自己定義的類型。
常常使用 struct{} 作為底層類型定義 key 的類型。對于 exported key 的靜態(tài)類型,常常是接口或者指針。這樣可以盡量減少內(nèi)存分配。
cancelCtx
type cancelCtx struct {
Context
mu sync.Mutex // 互斥鎖
done atomic.Value // 調(diào)用 cancel 時會關(guān)閉的 channel
children map[canceler]struct{} // 記錄了由此 Context 派生的所有 child,此 Context 被 cancel 時會同時 cancel 所有 child
err error // 錯誤信息
}
WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent 已經(jīng)取消了,直接取消子 Context
child.cancel(false, p.err)
} else {
// 將 child 添加到 parent 的 children 切片
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
// 沒有 parent 可以“掛載”,啟動一個 goroutine 監(jiān)聽 parent 的 cancel,同時 cancel 自身
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
cancel
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
c.mu.Lock()
// 設(shè)置 cancel 的原因
c.err = err
// 關(guān)閉自身的 done 通道
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
// 遍歷所有 children,逐個調(diào)用 cancel 方法
for child := range c.children {
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
// 正常情況下,需要將自身從 parent 的 children 切片中刪除
if removeFromParent {
removeChild(c.Context, c)
}
}
timerCtx
type timerCtx struct {
cancelCtx
timer *time.Timer
deadline time.Time
}
WithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// 如果parent的截止時間更早,直接返回一個cancelCtx即可
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c) // 同cancelCtx的處理邏輯
dur := time.Until(d)
if dur <= 0 { //當前時間已經(jīng)超過了截止時間,直接cancel
c.cancel(true, DeadlineExceeded)
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
// 設(shè)置一個定時器,到截止時間后取消
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
截止時間到了;
cancel 函數(shù)被調(diào)用;
parent 的 Done 被 close。
valueCtx
type valueCtx struct {
Context
key, val interface{}
}

