Go Context 使用和源碼分析

概述

Go語言中的Goroutine是go語言中的最重要的一部分,是一個用戶級的線程是Go語言實現高併發高性能的重要緣由。可是如何中止一個已經開啓的Goroutine呢?通常有幾種方法:安全

  • 使用共享內存來中止goroutine,好比經過判斷一個全局變量來判斷是否要中止goroutine
  • 使用文件系統來中止goroutine,跟使用內存相同用文件來判斷
  • 使用context上下文,context也是你們最推薦的一種方式。而且能夠結束嵌套的goroutine。

簡單使用

context庫中,有4個關鍵方法:併發

  • WithCancel 返回一個cancel函數,調用這個函數則能夠主動中止goroutine。
  • WithValue WithValue能夠設置一個key/value的鍵值對,能夠在下游任何一個嵌套的context中經過key獲取value。可是不建議使用這種來作goroutine之間的通訊。
  • WithTimeout 函數能夠設置一個time.Duration,到了這個時間則會cancel這個context。
  • WithDeadline WithDeadline函數跟WithTimeout很相近,只是WithDeadline設置的是一個時間點。
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    //cancel
    ctx, cancel := context.WithCancel(context.Background())
    go work(ctx, "work1")

    time.Sleep(time.Second * 3)
    cancel()
    time.Sleep(time.Second * 1)

    // with value
    ctx1, valueCancel := context.WithCancel(context.Background())
    valueCtx := context.WithValue(ctx1, "key", "test value context")
    go workWithValue(valueCtx, "value work", "key")
    time.Sleep(time.Second * 3)
    valueCancel()

    // timeout
    ctx2, timeCancel := context.WithTimeout(context.Background(), time.Second*3)
    go work(ctx2, "time cancel")
    time.Sleep(time.Second * 5)
    timeCancel()

    // deadline
    ctx3, deadlineCancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*3))
    go work(ctx3, "deadline cancel")
    time.Sleep(time.Second * 5)
    deadlineCancel()

    time.Sleep(time.Second * 3)

}

func workWithValue(ctx context.Context, name string, key string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(ctx.Value(key))
            println(name, " get message to quit")
            return
        default:
            println(name, " is running", time.Now().String())
            time.Sleep(time.Second)
        }
    }
}

func work(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            println(name, " get message to quit")
            return
        default:
            println(name, " is running", time.Now().String())
            time.Sleep(time.Second)
        }
    }
}

源碼分析

context的原理其實就是利用了channel struct{}的特性,使用select獲取channel數據。一旦關閉這個channel則會收到數據退出goroutine中的邏輯。context也是支持嵌套使用,結構就以下圖顯示利用的是一個map類型來存儲子context。關閉一個節點就會循環關閉這個節點下面的全部子節點,就實現了優雅的退出goroutine的功能。下面咱們看具體接口對象和源碼邏輯。框架

圖片描述

Context接口和核心對象

context interface 有4個方法函數

  • Deadline 該方法返回一個deadline和標識是否已設置deadline的bool值,若是沒有設置deadline,則ok == false,此時deadline爲一個初始值的time.Time值
  • Done 返回一個channel。當timeout或者調用cancel方法時,將會close掉
  • Err 返回一個錯誤
  • Value 返回WithValue設置的值
type Context interface {

    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}
}

emptyCtx

在上面的例子中咱們能夠看到函數context.Background(), 這個函數返回的就是一個emptyCtx
emptyCtx常常被用做在跟節點或者說是最上層的context,由於context是能夠嵌套的。在上面的Withvalue的例子中已經看到,先用emptyCtx建立一個context,而後再使用withValue把以前建立的context傳入。這個操做會在下面的分析中詳細瞭解的。
下面就是emptyCtx,其實實現很簡單全部的方法幾乎返回的都是nil。
ToDo函數返回的也是高併發

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

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
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

cancelCtx

cancelCtx是context實現裏最重要的一環,context的取消幾乎都是使用了這個對象。WithDeadline WithTimeout其實最終都是調用的cancel的cancel函數來實現的。
對象中的字段:源碼分析

  • Context 保存parent Context
  • mu 用來保護數據
  • done 用來標識是否已被cancel。當外部觸發cancel、或者父Context的channel關閉時,此done也會關閉
  • children 保存它的全部子canceler
  • err 已經cancel則err!= nil

cancel主要函數:性能

Done

Done函數返回一個chan struct{}的channel,用來判斷context是否已經被close了。從上面的例子能夠看到使用一個select 來判斷context是否被關閉。一旦從外部調用cancel函數關閉了context的done屬性,select則能夠拿到輸出,最終關閉這個context測試

Cancel

Cancel函數用來在外部調用,調用以後主要操做:ui

  1. 加鎖避免多出操做
  2. 若是cancelCtx的done未被初始化則初始化一個(這個屬於lazyload)
  3. 調用close(c.done) 來關閉channel,因爲make(chan struct{})的特性,上面的Done channel則會接收到數據
  4. 循環調用context.children 的cancel方法,關閉全部嵌套的context。
  5. 釋放鎖c.mu.Unlock()
  6. 根據參數removeFromParent來判斷是否要
type cancelCtx struct {
    Context
    mu       sync.Mutex            // protects following fields
    done     chan struct{}         // created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}

// 能夠被cancel的對象,實現者是*cancelCtx 和 *timerCtx.
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil {
        c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
}

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.err
}

func (c *cancelCtx) String() string {
    return fmt.Sprintf("%v.WithCancel", c.Context)
}

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) C(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 // already canceled
    }
    c.err = err
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }
    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)
    }
}

timerCtx

timeCtx實際上是在cancelCtx基礎上增長timer屬性。其中的cancel函數也是調用cancelCtx的Cancel函數。this

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

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()
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}

WithCancel WithDeadline WithTimeout WithValue

這三個方法是對於context使用的一個封裝,在最上邊的例子裏咱們能夠看到是如何使用的。在這段咱們是要看的是如何實現的源碼。

WithCancel

WithCancel函數返回context和一個主動取消的函數,外部只要調用這個函數則會close context中channel。
返回的函數測試cancelCtx中測cancel函數,在上面已經有了詳細說明這裏就不過多描述了。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

WithDeadline

  1. 判斷父節點中的deadline是否比父節點的早,若是是則直接調用WithCancel
  2. 建立一個timerCtx,timerCtx的具體描述也在上面詳細分析過了
  3. 使用time.afterFunc設置dur,當時間到了則執行timerCtx.Cancel最終執行的也是cancelCtx.Cancel
  4. 返回Cancel函數,方便外部調用
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 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) }
}

WithTimeout

WithTimeout實現很簡單,其實就是調用了WithDeadline方法,傳入已經計算過的deadline。

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

WithValue

WithValue 不返回cancel函數,只是把傳入的key和value保存起來。方便上下游節點根據key獲取value。

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)
}

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}
}

使用原則

從網上看到了一些使用原則,把他摘抄下來:

  • 不要把Context存在一個結構體當中,顯式地傳入函數。Context變量須要做爲第一個參數使用,通常命名爲ctx。
  • 即便方法容許,也不要傳入一個nil的Context,若是你不肯定你要用什麼Context的時候傳一個context.TODO。
  • 使用context的Value相關方法只應該用於在程序和接口中傳遞的和請求相關的元數據,不要用它來傳遞一些可選的參數。
  • 一樣的Context能夠用來傳遞到不一樣的goroutine中,Context在多個goroutine中是安全的

總結

上面講述了context的用法和源碼,其實有不少框架都實現了本身的context。其實只要繼承了context接口就是一個context對象。Context是你們都比較推薦的一種中止goroutine的一種方式,而且context支持嵌套,中止跟節點它下面全部的子節點都會中止。

相關文章
相關標籤/搜索