標準庫的context
包
從設計角度上來講, golang的context
包提供了一種父routine對子routine的管理功能. 我的這種理解雖然和網上各種文章中講的不太一樣, 但我認為基本上還是很貼合實際的.
context
包中定義了一個很重要的介面, 叫context.Context
.它的使用邏輯是這樣的:
- 當父routine需要建立一個子routine的時候, 父routine應當先建立一個
context.Context
的例項, 這個例項中包括的內容有:- 對子routine生命週期的限制: 比如子routine應該什麼時候自殺, 什麼條件下自殺. 在服務端程式設計中, 一個生動的粟子就是: 接收請求的routine在將請求派發給工作routine的時候, 需要告訴工作routine: 超過400ms沒處理完你就給我就地爆炸.
- 將一些資料共享給子routine.
- 在子routine執行過程中, 通過這個
Context
例項, 可以干涉子routine的生命週期
- 子routine拿到父routine建立的
context.Context
例項後, 開始幹活, 幹活的過程中, 需要:- 遵守
Context
例項中關於自身生命週期的約束: 400ms請求沒有處理完, 我要就地爆炸 - 在自殺之前將自己自殺的訊息傳遞給
Context
, 這樣父routine就可以得知自己的生命狀態. 比如我200ms處理完了請求, 我要告訴父routine, 我已經好了 - 工作的時候, 如有必要, 從
Context
中獲取一些必要資料. - 工作結束時, 如有必要, 將一些工作成果傳送給
Context
, 以讓父routine得知: 比如, 我處理這個請求花費的時間是197ms - 在執行過程中, 從
Context
接收來自你routine的排程訊號
- 遵守
所以說很顯然:
Context
例項是由父routine建立的. 建立之後傳遞給子routine作為行為規範- 子routine一般是不允許操作這個
Context
例項的. 子routine應當耐心傾聽, 僅在必要的時候, 比如自殺之前, 將一些資訊傳遞給Context
- 一個
Context
的一生, 從生到死, 是和子routine繫結在一起的. 子routine生,Context
生, 子routine死,Context
死 - 良好設計的服務端程式, 每個routine都應該有自己的Context. 而既然routine之間有父子關係樹, 那麼顯然所有routine的Context之間也有一坨樹型關係.
我們現在來看context/context.go
中是如何實現這套工具的
1 首先是對基本Context的定義
// 定義了一個介面, 名為Context
type Context interface {
// 返回這個Context的死亡時刻, 如果ok == false, 則這個Context是永生的
Deadline() (deadline time.Time, ok bool)
// 返回一個channel, 這個channel在Context被Cancel的時候被關閉
// 如果Context是永生的, 則返回一個nil
Done() <-chan struct{}
// 在Context活著的時候, (Done()返回的channel還沒被關閉), 它返回nil
// 在Context死後, (Done()返回的channel被關閉), 它返回一個error例項用以說明:
// 這個Context是為什麼死掉的, 是被Cancel, 還是自然死亡?
Err() error
// 返回儲存在Context中的通訊資料
// 注意: 不要濫用這個介面, 它不是用來給子routine傳遞引數用的!
Value(key interface{}) interface
}
// 定義了兩個error例項, 併為其中一個例項的error型別定義了三個方法
var Canceled = errors.New("context canceled") // 用以在Context被Cancel時, 從Err()返回
var DeadlineExceeded error = deadlineExceedError{} // 用以在Context自然死亡時, 從Err()返回
type deadlineExceedError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" } // 實現error介面
func (deadlineExceededError) Timeout() bool { return true }
func (deadlineExceededError) Temporary() bool { return true }
// 實現了一個Context型別: emptyCtx, 它有以下特點:
// 0. 這個型別不對外公開, 僅通過後面的兩個介面公開它的兩個例項
// 1. 不能被Cancel
// 2. 也從不自然死亡, 它是永生的
// 3. 不同的例項之間需要有不同的地址, 所以它沒有被定義成struct{}, 而是用一個int來替代
// 4. 它內部也不儲存任何資料
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
// 定義了兩個emptyCtx的例項, 並寫了兩個介面對外公開這兩個例項
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
func Background() Context {
return background
}
func TODO() Context {
return todo
}
上面定義了Context
的介面規範, 也定義了一個Context
介面的實現: emptyCtx
, 從程式碼上可以看出來, 標準庫並不公開這個emptyCtx
的實現, 你只能從它的公開介面context.Background()
或context.TODO()
來訪問兩個已經例項化的emptyCtx
例項.
這兩個例項是用於為頂層routine使用的.下面我們再來看, 可被建立者Cancel的Context
是怎麼實現的
2 Context
介面的實現: 支援Cancel操作的Context: 非公開類cancelCtx
首先是類定義
type cancelCtx struct {
Context // 他爹
mu sync.Mutex // 一個互斥鎖, 用來保護其它欄位
done chan struct{} // Done()方法的返回值
children map[canceler]struct{} // 這裡記錄了它的孩子
err error // Err()方法的返回值
}
我們在上面說了, 由於程式中的routine之間是有父子關係樹存在的, 那麼一個context正常情況下就有可能有孩子, 那麼, 如果當前的routine持有的Context例項是可被Cancel的, 那麼顯然, 它的所有孩子routine, 也應當是可被Cancel的.
這就是為什麼cancelCtx
類中有Context
欄位和children
欄位的原因, 也是為什麼children
欄位是一個map[canceler]struct{}
型別的原因: key中記錄著所有的孩子, value是沒有意義的, 為什麼這樣寫呢? 因為這裡把map
當成C++中的std::set
在用!
key的型別canceler
是一個介面, 一個表示Context必須可被Cancel的介面:
type canceler interface {
cancel(removeFromParent bool, err error)
// Context介面中的Done方法
Done() <-chan struct{}
}
顯然, cancelCtx
類本身也是可被Cancel的, 所以它也要實現canceler
這個介面
下面是cancelCtx
類的方法實現:
// Context.Done的實現: 返回欄位 done
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock() // 鎖保護done欄位的初始化
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
// Context.Err的實現
func (c *cancelCtx) Err() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.err
}
// String()方法實現
func (c *cancelCtx) String() string {
return fmt.Sprintf("%v.WithCancel", c.Context)
}
// canceler.cancel介面實現
// 引數 removeFromParent 指示是否需要把它從它爹的孩子中除名
// 引數 err 將賦值給欄位 err, 以供Context.Err方法返回
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock() // 上鎖
if c.err != nil { // 如果err欄位有值, 則說明已經被Cancel了
c.mu.Unlock()
return // already canceled
}
c.err = err
if c.done == nil { // 設定c.done, 以供Done方法返回
c.done = closedchan
} else {
close(c.done)
}
// 挨個cancel它的所有孩子, 子隨父死的時候, 並不除名父子關係
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
// 如有必要, 把它從它爹那裡除名
if removeFromParent {
removeChild(c.Context, c)
}
}
// 這是一個全域性複用的, 被關閉的channel, 用於被Context.Done返回使用
var closedchan = make(chan struct{})
func init() {
close(closedchan)
}
可以看到, cancelCtx
本身並沒有實現所有的Context
介面中的方法. 其餘沒有實現的介面是通過Context
這個沒有指定欄位名的欄位實現的. 這是go的特殊語法糖: 繼承介面.
在一個型別定義中, 宣告一個介面型別欄位, 並且還不指定欄位的名稱, 這代表
- 當前型別必然實現了介面型別
- 當呼叫介面方法時, 預設呼叫的是子欄位的方法, 除非當前型別顯式overwrite了一些方法的實現
其實就是一種更為靈活的繼承寫法
我們再來看, 當父routine需要建立一個帶有Cancel功能的Context
例項的時候, 應該怎麼辦:
// 首先是定義一個函式指標別名
type CancelFunc func()
// 再就是父routine建立帶Cancel功能的子Context的函式
// 父routine將自己的Context例項傳入, 這個函式會返回子Context(帶Cancel功能)
// 還會返回一個可呼叫物件 cancel, 呼叫這個物件(函式), 就能達到Cancel的功能
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent) // 建立一個cancelCtx的例項
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// 下面是WithCancel中引用的兩個私有函式的實現
// 建立一個cancelCtx例項
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent} // 把爹先記錄下來
}
func propagateCancel(parent Context, child canceler) {
// 如果父Context是不可Cancel, 什麼也不做
if parent.Done() == nil {
return // parent is never canceled
}
// 如果父Context本身是可Cancel的
if p, ok := parentCancelCtx(parent); ok {
// 進入此分支, 說明父Context是以下三種之一:
// 1. 是一個cancelCtx, 本身就可被Cancel
// 2. 是一個timerCtx, timerCtx是canctx的一個子類, 也可被Cancel
// 3. 是一個valueCtx, valueCtx繼承體系上的某個爹, 是以上兩者之一
// 那麼p就是那個父Context的繼承體系中的cancelCtx例項
p.mu.Lock()
if p.err != nil {
// 若p已經被Cancel或自然死亡, 作為兒子, 就必須死了
// 直接呼叫p.cancel
child.cancel(false, p.err)
} else {
// 若p還活著, 就把兒子新增到它的兒子列表中去
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 進入此分支, 說明父Context雖然可被Cancel
// 但並不是標準庫中預設的cancelCtx或timerCtx兩種可被Cancel的型別
// 這意味著這個特殊的父Context, 內部並不能保證記錄了所有兒子的列表
// 這裡就得新開一個routine, 時刻監視著父Context的生存狀態
// 一旦父Context死亡, 就立即呼叫child.cancel把兒子弄死
go func() {
select {
case <-parent.Done(): // 如果爹死了, 把孩子弄死
child.cancel(false, parent.Err())
case <-child.Done(): // 如果孩子死了, 什麼也不做
}
}()
}
}
// 判斷Context例項是否是一個可被Cancel的型別
// 標準庫中可被Cancel的Context型別共有三種:
// 1. cancelCtx
// 2. timerCtx
// 僅有這兩種
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return &c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
return nil, false
}
}
}
3 當你使用WithCancel
時
一個簡單的例子
這裡來捋一捋, 當你呼叫WithCancel
建立一個可被Cancel的Context例項時, 都發生了些什麼:
// 第一步, 建立者routine本身必須持有一個Context
// 這裡假定建立者就是main routine
// 我們呼叫 Background建立一個不可被Cancel, 不會自殺的Context
contextOfMain := ctx.Background()
// 第二步: 呼叫WithCancel建立子Context
contextOfSubRoutine, cancelFuncOfSubRoutine := ctx.WithCancel(contextOfMain)
用起來是十分簡單的, 我們再來捋一捋第二步背後都發生了什麼, 下面是偽碼:
WithCancel(contextOfMain) {
// 第一步: 呼叫newCancelCtx建立了一個 cancelCtx 私有類的例項, 長這樣:
c := cancelCtx {
Context: contextOfMain,
mu : 預設值,
done : nil, // 雖然現在是nil, 但在呼叫Done()方法時會返回一個make(chan struct{})
children: nil,
err : nil,
}
// 第二步, 呼叫propagateCancel(contextOfMain, c)
// 內部大概發生這樣:
{
if contextOfMain.Done() == nil {
// 什麼也沒有發生
}
}
// 第三步: 返回c, 並且構造一個CancelFunc返回
// 先是返回c
return &c // 這裡返回的是c的地址
// 再是原地構造一個CancelFunc
func () {
c.cancel(true, Canceled)
}
/*
注意:
c.cancel呼叫的是cancelCtx.cancel方法
Canceled是一個全域性變數, 值 == errors.New("context canceled")
*/
}
然後, 你將這個建立好的cancelFuncOfSubRoutine
傳遞給新啟動的子routine, 過了幾分鐘, 你呼叫cancelFuncOfSubRoutine()
意圖主動Cancel掉子routine的時候, 內部是這樣執行的:
// 其實內部執行的是
contextofSubRoutine.cancel(true, errors.New("context canceled")) {
contextofSubRoutine.mu.Lock()
contextofSubRoutine.err = errors.New("context canceled")
contextofSubRoutine.done = closedchan // 這是一個已經被關閉的chan struct{}
for child := range contextofSubRoutine.children {
// 遞迴Cancel掉子routine下的所有孫子
// 而實際上它並沒有孩子, 所以什麼也不做
child.cancel(false, errors.New("context canceled"))
}
contextofSubRoutine.children = nil // 一把火把孫子的屍首全燒了
contextofSubRoutine.mu.Unlock()
removeChild(contextofSubRoutine.Context, contextofSubRoutine) {
p, ok := parentCancelCtx(contextOfMainRoutine, contextofSubRoutine)
// 由於contextOfMainRoutine的型別是emptyCtx
// 所以parentCancelCtx函式返回的是 nil, false
所以, 什麼也不做, 就返回了
}
}
一個稍微複雜一點的例子
我們假設當前程式中的routine樹(即是Context樹)關係如下所示:
mainContext // emptyCtx
|
\-> subContext // cancelCtx
|
\-> subsubContext1 // cancelCtx
\-> subsubContext2 // cancenCtx
\-> subsubContext3 // cancelCtx
現在, subContext
要建立第四個subsubContext4
, 它會這樣做:
// 在subRoutine中
subsubContext4, cancelFunc4 := ctx.WithCancel(subContext) {
// 內部是這樣的:
// step 1: 呼叫 ctx.newCancelCtx()
subsubContext4 := &cancelCtx {
Context: subContext,
mu : 預設值,
done : nil, // 雖然現在是nil, 但在呼叫Done()時會返回一個make(chan struct{})
children: nil,
err : nil,
}
// step 2: 呼叫propagateCancel(subContext, subsubContext4)
{
// p, ok := parentCancelCtx(subContext)
{
p := subContext
ok := true
}
// 這裡將subsubContext4加到subContext的兒子列表中去
subContext.mu.Lock()
subContext[subsubContext4] = struct{}{}
subContext.mu.Unlock()
}
// step 3: 建立CancelFunc
cancelFunc4 := func() {
subsubContext4.cancel(true, errors.New("context canceled"))
}
}
建立結束後, subContext
長這樣:
subContext := &cancelCtx {
Context: mainContext,
mu : 預設值,
done : nil, // 雖然現在是nil, 但在呼叫Done()時會返回一個make(chan struct{})
children : {
subsubContext1 : struct{}{},
subsubContext2 : struct{}{},
subsubContext3 : struct{}{},
subsubContext4 : struct{}{},
},
err : nil
}
然後, 當subRoutine
呼叫cancelFunc4
意圖弄死subsubRoutine4
的時候, 會發生如下:
subsubContext4.cancel(true, errors.New("context canceled")){
subsubContext4.mu.Lock()
subsubContext4.err = errors.New("context canceled")
subsubContext4.done = closedchan
for child := range subsubContext4.children {
// subsubContext4並沒有孩子
// 什麼也不做
}
subsubContext4.children = nil
subsubContext4.mu.Unlock()
removeChild(subsubContext, subsubContext4) {
// 從subContext的children中刪除 subsubContext4
}
}
而如果, 在Cancel了subRoutine4後, 主執行緒中直接要Cancel SubRoutine的話, 會發生什麼? 會發生如下:
subContext.cancel(true, errors.New("context canceled")) {
subContext.mu.Lock()
subContext.err = errors.New("context canceled")
subContext.done = closedchan
for child := range subContext.children {
// 這裡會呼叫 subsubContext1/2/3的cancel方法
child.cancel(false, errors.New("context canceled"))
}
subContext.children = nil
subContext.mu.Unlock()
// 最後一步本身要將subContext從他爹那裡除名
// 但由於他爹是個emptyCtx, 所以什麼也不做
}
基本把整個cancelCtx
的流程理解掉之後, 後面的所謂的帶DeadLine
的Context就非常好理解了
4 Context介面的實現: 支援Deadline()
操作的Context: 非公開類timerCtx
timerCtx
實現了定時器功能: 到達指定時刻, 自殺.
首先是類定義:
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
需要注意的是兩點:
- 它繼承了
cancelCtx
- 定時功能是由標準庫的
time.Timer
實現的
先看它的Deadline()
方法的實現, 這個方法就是它的靈魂
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
只是簡單的欄位deadline
的getter
它還重寫了canceler.cancel
方法:
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil { // 主要是在Cancel時停掉內部的計時器
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
再來看和WithCancel
平級的WithDeadLine
函式:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// 如果父Context也是一個有死期的Context, 並且死期還在兒子想死之前
// 那麼只是簡單的呼叫WithCancel來給建立一個可被Cancel的cancelCtx即可
// 這樣, 建立出的子Context呼叫Deadline()方法時, 實質上呼叫的是他爹的Deadline(), 語義上完美完成任務
return WithCancel(parent)
}
// 不然, 得帶個定時器, 先把死期記在deadline欄位中
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 設定邏輯: 爹死的時候兒子也得死, 並且如果可能, 把兒子記在爹的children欄位中
propagateCancel(parent, c)
// 如果死期已經過了
dur := time.Until(d)
if dur <= 0 {
// 原地自殺, 但還是要返回這個Context
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(true, Canceled) }
}
c.mu.Lock()
// 建立定時器
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
注意:
WithDeadline
也返回一個CancelFunc
- 如果爹死的比兒子預想的還早, 那隻不過是用爹呼叫
WithCancel
建立了一個可Cancel的Context - 如果死期在呼叫
WithDeadline
的時候已經到達了, 那麼依然要給呼叫方返回一個死掉的兒子屍體, 只不過它的Done()
和Err()
會指出這個Context已經死掉了 - 定時器的到期回撥, 呼叫的就是
canceler.cancel
方法
可以看到, timerCtx
只是對cancelCtx
在功能上的追加. WithDeadline
也只是簡單的追加了一個定時器,邏輯還是比較簡單的.
所以,如果到這裡你已經腦子有點亂掉了, 還是要回頭把cancelCtx
理清
另外, 這裡還提供了一個名為WithTimeout
的函式, 其實與WithDeadline
是完全等價的, 實現如下:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
這裡我們就不再著重分析WithDeadline/WithTimeout
的邏輯流程了
5 Context介面的實現: 帶資料共享的非公開類valueCtx
整個定義十分簡單, 就是在Context
介面之上, 實現了對資料的儲存而已, 並且只能儲存一個key, 全文如下:
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) String() string {
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
可以看到, valueCtx
將所有重要功能的實現都委託到了父類上, 這在使用時就非常信賴父類, 也就是說, 如果你想僅僅依靠標準庫的這些公開介面, 來直接在主routine下開啟一個, 既帶資料共享, 還帶可Cancel功能的子Context的話, 你只能這樣寫:
mainContext := ctx.Background() // 主routine
// 為了建立一個帶Cancel功能的valueCtx, 首先需要建立一個cancelCtx
tmpContext, subRoutineCancelFunc := ctx.WithCancel(mainContext)
subContext := ctx.WithValue(tmpContext, key, value)
6 總結
標準庫的context
包, 只實現了幾個基本的Context
介面的實現, 並且還很受限的只能通過公開介面WithXXX
來建立, 這很顯然是在鼓勵你做下面的事情:
- 在已有的
Context
介面定義上, 定義你自己的Context實現類. - 不要將過多的邏輯放置在
Context
中去, 讓它只幹好自己該乾的事情: 那就是父子routine間生命週期的管理
並且顯然context
包只實現了Context
的語義, 並沒有實現相關的routine的操作: 比如在Cancel時掐死子程式, 在Deadline
到期的時候自動自殺等. 這還需要由使用者自行實現.