手摸手Go Context探秘

使用Go作為服務(wù)端開發(fā)時(shí)漓踢,每個(gè)請(qǐng)求過來都會(huì)分配一個(gè)goroutine來處理,請(qǐng)求處理過程中漏隐,可能還會(huì)創(chuàng)建額外的goroutine訪問DB或者RPC服務(wù)喧半。這個(gè)請(qǐng)求涉及的goroutine可能需要訪問一些特定的值比如認(rèn)證token、用戶標(biāo)識(shí)或者請(qǐng)求截止時(shí)間青责。當(dāng)一個(gè)請(qǐng)求取消或者超時(shí)挺据,則這個(gè)請(qǐng)求涉及的goroutine也應(yīng)該被終止,這樣系統(tǒng)就可以快速回收這部分資源脖隶。

基于簡化目的扁耐,context包定義了Context類型,來傳遞超時(shí)产阱、取消信號(hào)以及跨API邊界和進(jìn)程之間request-scope值婉称。當(dāng)服務(wù)器來新的請(qǐng)求應(yīng)該創(chuàng)建一個(gè)Context并且返回請(qǐng)求應(yīng)該接受一個(gè)Context。這其中的函數(shù)調(diào)用鏈必須傳遞Context對(duì)象心墅,或者是通過WithCancel酿矢、WithDeadlineWithTimeWithValue衍生的Context對(duì)象怎燥。當(dāng)一個(gè)Context取消瘫筐,所有從這個(gè)對(duì)象衍生的Context都會(huì)被取消。

例如你可以利用context的這種機(jī)制铐姚,實(shí)現(xiàn)一個(gè)任務(wù)超時(shí)保護(hù)的方法

func main() {
    cancelJob(time.Second*1, func() error {
        time.Sleep(time.Second * 10)
        return nil
    })
}

func cancelJob(timeout time.Duration, f func() error) error {
    var (
        ctx        context.Context
        cancelFunc context.CancelFunc
    )
    if timeout > 0 {
        ctx, cancelFunc = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancelFunc = context.WithCancel(context.Background())
    }

    defer cancelFunc()
    e := make(chan error, 1)
    go func() {
        e <- f()
    }()
    select {
    case err := <-e:
        return err
    case <-ctx.Done():
        return ctx.Err()
    }
}

Context使用大致步驟:

  • 1 構(gòu)建一個(gè)Context對(duì)象,如果你不知道該使用什么Context合適策肝,可以調(diào)用context.Backgroundcontext.TODO
  • 2 根據(jù)你的需求可以對(duì)Context進(jìn)行包裝衍生
    • WithCancel :Context可取消
    • WithDeadline: Context可設(shè)置截止時(shí)間
    • WithTimeout:實(shí)際使用的是WithDeadline
    • WithValue: 需要使用Context傳值
  • 3 監(jiān)聽ctx.Done這個(gè)channel 一旦Context取消 此channel會(huì)被關(guān)閉
  • 4 最后在方法處理完畢時(shí)請(qǐng)及時(shí)調(diào)用cancel方法 方便資源回收

數(shù)據(jù)結(jié)構(gòu)

context包提供了兩種創(chuàng)建Context對(duì)象的便捷方式

  • context.Background 無法被取消 沒有值 沒有截止時(shí)間,通常用于主函數(shù)隐绵、初始化之众、測試或者當(dāng)新請(qǐng)求來了作為頂層Context
  • context.TODO 當(dāng)你不知道用啥Context的時(shí)候使用

這兩種方式都是一個(gè)emptyCtx對(duì)象 本質(zhì)上沒啥差別

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)
func Background() Context {
    return background
}
func TODO() Context {
    return todo
}

此外context包提供了4個(gè)方法:WithCancelWithDeadline依许、WithTimeout棺禾、WithValue,可以對(duì)context進(jìn)行衍生為cancelCtx峭跳、timerCtx膘婶、valueCtx,他們都實(shí)現(xiàn)了context.Context接口

// Context的方法是線程安全的
type Context interface {
  // 返回context何時(shí)需要被取消 ok為false表示deadline未設(shè)置
    Deadline() (deadline time.Time, ok bool)
    // 當(dāng)context被取消 Done放回一個(gè)關(guān)閉的channel 
  // Done返回nil 表示當(dāng)前context不能被取消
    // Done通常在select語句中使用
    Done() <-chan struct{}
  // 返回context取消的原因
    Err() error
  // 返回context中指定的key關(guān)聯(lián)的value蛀醉,未指定返回nil
    // 主要用作在進(jìn)程和API邊界間傳遞request-scoped數(shù)據(jù)悬襟,不要用于可選參數(shù)傳遞
    // key需要支持相等操作,最好定義為不可到處類型 避免混淆
    Value(key interface{}) interface{}
}

這幾個(gè)對(duì)象層次結(jié)構(gòu)

context architecture

衍生contexts

通過WithCancel拯刁、WithDeadline脊岳、WithTimeoutWithValue方法衍生的context為原始context提供了取消、傳值割捅、超時(shí)取消等功能奶躯。

WithCancel

通過WithCancel衍生出新的可取消的Context對(duì)象

// WithCancel 返回包含父context拷貝和一個(gè)新的channel的context 和一個(gè)cancel函數(shù)
// 當(dāng)返回的cancel函數(shù)被調(diào)用或父context的done channcel關(guān)閉 則WitchCancel返回的context的channel也會(huì)被關(guān)閉
// 當(dāng)操作完成時(shí)應(yīng)該盡快調(diào)用cancel函數(shù) 這樣就可以釋放此context關(guān)聯(lián)的資源
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
  // 構(gòu)建父子上下文之間的關(guān)系 保證父上下文取消時(shí)子上下文也會(huì)被取消
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

將Context包裝為可取消的Context-->cancelCtx

// cancelCtx可以被取消,當(dāng)取消時(shí)棺牧,也會(huì)將其實(shí)現(xiàn)了canceler的子context也取消
type cancelCtx struct {
    Context

    mu       sync.Mutex            // 保護(hù)下面的字段
    done     chan struct{}         // 惰性創(chuàng)建 cancel方法第一次調(diào)用時(shí)關(guān)閉
    children map[canceler]struct{} // cancel第一次調(diào)用時(shí)置為nil
    err      error                 // cancel第一次調(diào)用時(shí)設(shè)置non-nil
}

其中done這個(gè)channel是在cancel調(diào)用的時(shí)候才會(huì)被初始化巫糙,cancelCtx子context若可取消需要實(shí)現(xiàn)canceler接口

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

構(gòu)建可取消context后,后將取消操作進(jìn)行傳播颊乘,如果父context的Done為nil参淹,表示其不可取消直接返回,否則會(huì)調(diào)用parentCancelCtx直到找到可取消父context 乏悄,若找到

  • 若可以找到則

    • 且父context已經(jīng)取消則會(huì)調(diào)用子context的cancel方法進(jìn)行取消浙值;
    • 且父context未取消則將當(dāng)前子context交給父context管理
  • 若找不到 例如開發(fā)者自定義的類型則

    直接啟動(dòng)一個(gè)gorountine來監(jiān)聽父子取消事件通知

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
    if parent.Done() == nil {
        return // parent is never canceled
    }
    if p, ok := parentCancelCtx(parent); ok {//找到父可取消context
        p.mu.Lock()
        if p.err != nil {
            // 父context已經(jīng)被取消 取消子context
            child.cancel(false, p.err)
        } else {// 父context未cancel 則將子context交給父context管理,方便父節(jié)點(diǎn)取消時(shí)將取消事件傳播給子context
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {//找不到父可取消context
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

parenCancelCtx循環(huán)查找context是否存在可取消父節(jié)點(diǎn)

// parentCancelCtx follows a chain of parent references until it finds a
// *cancelCtx. This function understands how each of the concrete types in this
// package represents its parent.
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
        }
    }
}
取消函數(shù)cancel

當(dāng)你的業(yè)務(wù)方法執(zhí)行完畢檩小,你應(yīng)該盡快調(diào)用cancel方法开呐,這樣方便快速回收相關(guān)資源

//關(guān)閉c.done,取消掉c的子context,若removeFromParent為true规求,則將c從父context的子context集合中刪除
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 {
        c.mu.Unlock()
        return // c.err不為nil 則表示當(dāng)前context已經(jīng)被取消
    }
    c.err = err
    if c.done == nil { //調(diào)用cancel方法此時(shí)才初始化
        c.done = closedchan//closedchan如其名字 為已經(jīng)關(guān)閉的chan
    } else {//關(guān)閉c.done
        close(c.done)
    }
  //對(duì)子context進(jìn)行cancel
    for child := range c.children {
        // 此處在持有父context鎖的同時(shí) 獲取子context的鎖
        child.cancel(false, err)
    }
    c.children = nil //cancel完畢 置nil
    c.mu.Unlock()

    if removeFromParent {//將當(dāng)前context從其父context的子context集合中刪除
        removeChild(c.Context, c)
    }
}

WithDeadline

讓context具備超時(shí)取消功能

// WithDeadline 返回包含父context拷貝和deadline為d的context筐付,如果父deadline早于d
// 則語義上WithDeadline(parent, d) 和父context是相等的
//當(dāng)deadline過期了 或者返回的cancel函數(shù)被調(diào)用 或者父context的done channel關(guān)閉了則WithDeadline返回的context中的done channel也會(huì)關(guān)閉
// 當(dāng)操作完成時(shí)應(yīng)該盡快調(diào)用cancel函數(shù) 這樣就可以釋放此context關(guān)聯(lián)的資源
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    propagateCancel(parent, c)
    dur := time.Until(d)
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded) // deadline已經(jīng)到期
        return c, func() { c.cancel(false, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
    //定時(shí)監(jiān)聽
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

timerCtx內(nèi)嵌了cancelCtx,主要的cancel能力進(jìn)行了代理阻肿。額外新增了一個(gè)截止時(shí)間和一個(gè)定時(shí)器瓦戚,初始化此類context時(shí)如果未到截止時(shí)間且未取消 則會(huì)啟動(dòng)一個(gè)定時(shí)器,超時(shí)即會(huì)執(zhí)行cancel操作

// timerCtx包含了一個(gè)定時(shí)器和一個(gè)截止時(shí)間 內(nèi)嵌一個(gè)cancelCtx來實(shí)現(xiàn) Done和Err方法
// 取消操作通過停止定時(shí)器然后調(diào)用cancelCtx.cancel來實(shí)現(xiàn)
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

timerCtx的cancel操作本身會(huì)停掉定時(shí)器丛塌,然后主要cancel操作代理給了cancelCtx

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 {
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}

WithTimeut

實(shí)際掉用了WithDeadline沒啥好說的较解。

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

總的來說,就是可取消context通過父節(jié)點(diǎn)保存子節(jié)點(diǎn)集合 若父節(jié)點(diǎn)取消則將子節(jié)點(diǎn)集合中的context依次調(diào)用cancel方法赴邻。

may be ugly

WithValue

賦予了Context傳值能力印衔,Context的能力代理給了父context,自身新增了一個(gè)Value(key interface{}) interface{}方法姥敛,根據(jù)指定key獲取跟context關(guān)聯(lián)的value奸焙,邏輯比較簡單 沒啥好說的。

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
    Context
    key, val interface{}
}

總結(jié)

context包核心實(shí)現(xiàn)除去注釋只有200-300行彤敛,整體實(shí)現(xiàn)還是短小精悍忿偷,為我們提供了跨進(jìn)程、API邊界的數(shù)據(jù)傳遞以及并發(fā)臊泌、超時(shí)取消等功能。實(shí)際應(yīng)用過程中也給我們技術(shù)實(shí)現(xiàn)帶來很大便利揍拆,比如全鏈路trace的實(shí)現(xiàn)渠概。官方建議我們將context作為函數(shù)第一個(gè)參數(shù)使用,不過實(shí)際使用過程中還是會(huì)給不少人代理心智負(fù)擔(dān),所以有人為了盡可能不寫context播揪,搞了個(gè)Goroutine local storage 有興趣可以研究下

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末贮喧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子猪狈,更是在濱河造成了極大的恐慌箱沦,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件雇庙,死亡現(xiàn)場離奇詭異谓形,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)疆前,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門寒跳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人竹椒,你說我怎么就攤上這事童太。” “怎么了胸完?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵书释,是天一觀的道長。 經(jīng)常有香客問我赊窥,道長爆惧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任誓琼,我火速辦了婚禮检激,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘腹侣。我一直安慰自己叔收,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布傲隶。 她就那樣靜靜地躺著饺律,像睡著了一般。 火紅的嫁衣襯著肌膚如雪跺株。 梳的紋絲不亂的頭發(fā)上复濒,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音乒省,去河邊找鬼巧颈。 笑死,一個(gè)胖子當(dāng)著我的面吹牛袖扛,可吹牛的內(nèi)容都是我干的砸泛。 我是一名探鬼主播十籍,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼唇礁!你這毒婦竟也來了勾栗?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤盏筐,失蹤者是張志新(化名)和其女友劉穎围俘,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體琢融,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡界牡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吏奸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欢揖。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖奋蔚,靈堂內(nèi)的尸體忽然破棺而出她混,到底是詐尸還是另有隱情,我是刑警寧澤泊碑,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布坤按,位于F島的核電站,受9級(jí)特大地震影響馒过,放射性物質(zhì)發(fā)生泄漏臭脓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一腹忽、第九天 我趴在偏房一處隱蔽的房頂上張望来累。 院中可真熱鬧,春花似錦窘奏、人聲如沸嘹锁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽领猾。三九已至,卻和暖如春骇扇,著一層夾襖步出監(jiān)牢的瞬間摔竿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國打工少孝, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留继低,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓稍走,卻偏偏與公主長得像郁季,于是被迫代替她去往敵國和親冷溃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 在GO中梦裂,我們需要有能力管理并發(fā)運(yùn)行中的goroutine,主要是指它的生命周期盖淡。那些失去控制的goroutine...
    浩軒01閱讀 280評(píng)論 0 0
  • 控制并發(fā)有兩種經(jīng)典的方式年柠,一種是WaitGroup,另外一種就是Context褪迟,今天我就談?wù)凜ontext冗恨。 什么...
    豆瓣奶茶閱讀 5,055評(píng)論 4 13
  • 深入理解 Go Context 什么是 Context Context 的最常見但也是最不準(zhǔn)確的翻譯是 ‘上下文’...
    小小小超子閱讀 5,824評(píng)論 2 20
  • Context-用來管理調(diào)用上下文,控制一個(gè)請(qǐng)求的生命周期。 直接看代碼:Context是一個(gè)接口 Context...
    Maggie_up閱讀 813評(píng)論 0 2
  • 1.1什么是 context golang在1.6.2的時(shí)候還沒有自己的context味赃,在1.7的版本中就把gol...
    zfh_51d2閱讀 720評(píng)論 0 0