Go語言context包的簡單介紹

簡介

  • 在 Go http包的Server中,每一個請求在都有一個對應的 goroutine 去處理。請求處理函數通常會啓動額外的 goroutine 用來訪問後端服務,比如數據庫和RPC服務。用來處理一個請求的 goroutine 通常需要訪問一些與請求特定的數據,比如終端用戶的身份認證信息、驗證相關的token、請求的截止時間。 當一個請求被取消或超時時,所有用來處理該請求的 goroutine 都應該迅速退出,然後系統才能釋放這些 goroutine 佔用的資源。
  • 在Google 內部,我們開發了 Context 包,專門用來簡化 對於處理單個請求的多個 goroutine 之間與請求域的數據、取消信號、截止時間等相關操作,這些操作可能涉及多個 API 調用。現在context已成爲官方庫,使用的時候只需要import "context"即可。

Context基本數據結構

【Context interface】

Context interface是最基本的接口:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • Deadline()返回一個time.Time,是當前 Context 的應該結束的時間,ok 表示是否有 deadline
  • Done()返回一個 channel,這個 channel 對於以 Context 方式運行的函數而言,是一個取消信號。當這個 channel 關閉時,上面提到的這些函數應該終止手頭的工作並立即返回。
  • Err()返回 Context 被取消時的錯誤
  • Value(key interface{}) 允許 Context 對象攜帶request作用域的數據,該數據必須是線程安全的。

【canceler interface】

canceler interface 定義了提供 cancel 函數的 context,要求數據結構要同時實現 Context interface。

type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

【Structs】

除了以上兩個 interface 之外,context 包中還定義了若干個struct,來實現上面的 interface

  • emptyCtx
    emptyCtx是空的Context,只實現了Context interface,只能作爲 root context 使用。
type emptyCtx int
  • cancelCtx
    cancelCtx繼承了Context並實現了cancelerinterface,從WithCancel()函數產生.
type cancelCtx struct {
    Context

    done chan struct{} // closed by the first cancel call.

    mu       sync.Mutex
    children map[canceler]bool // set to nil by the first cancel call
    err      error             // set to non-nil by the first cancel call
}
  • timerCtx
    timerCtx繼承了cancelCtx,所以也自然實現了Context和canceler這兩個interface,由WithDeadline()函數產生。·
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

deadline time.Time
}
  • valueCtx
    valueCtx包含key、val field,可以儲存一對鍵值對,由WithValue()函數產生。
type valueCtx struct {
    Context
    key, val interface{}
}

Context實例化和派生

【Background()函數】

Context 只定義了 interface,真正使用時需要實例化,官方首先定義了一個 emptyCtx struct 來實現 Context interface,然後提供了Backgroud()函數來便利的生成一個 emptyCtx 實例。實現代碼如下:

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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
}

Backgroud() 生成的 emptyCtx 實例是不能取消的,因爲emptyCtx沒有實現canceler interface,要正常取消功能的話,還需要對 emptyCtx 實例進行派生。常見的兩種派生用法是WithCancel() 和 WithTimeout。

【WithCancel()函數】

調用WithCancel()可以將基礎的 Context 進行繼承,返回一個cancelCtx示例,並返回一個函數,可以在外層直接調用cancelCtx.cancel()來取消 Context。代碼如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{
        Context: parent,
        done:    make(chan struct{}),
    }
}

【WithTimeout()函數】

調用WithTimeout,需要傳一個超時時間。來指定過多長時間後超時結束 Context,源代碼中可以得知WithTimeout是WithDeadline的一層皮,WithDeadline傳的是具體的結束時間點,這個在工程中並不實用,WithTimeout會根據運行時的時間做轉換。代碼如下:

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

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  deadline,
    }
    propagateCancel(parent, c)
    d := deadline.Sub(time.Now())
    if d <= 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(d, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

在WithDeadline中,將 timeCtx.timer 掛上結束時的回調函數,回調函數的內容是調用cancel來結束 Context。

【WithValue()函數】

創建一個存儲 k-v 對的 context。代碼如下:

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控制多個goroutine示例

以下代碼啓動了 3 個監控 goroutine 進行不斷的運行任務,每一個都使用了 Context 進行跟蹤,當我們使用 cancel 函數通知取消時,這 3 個 goroutine 都會被結束。這就是 Context 的控制能力,它就像一個控制器一樣,按下開關後,所有基於這個 Context 或者衍生的子 Context 都會收到通知,這時就可以進行清理操作了,最終釋放 goroutine,這就解決了 goroutine 啓動後不可控的問題。

package main

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

// 使用context控制多個goroutine
func watch(ctx context.Context, name string) {
    for {
        select {
        case <- ctx.Done():
            fmt.Println(name, "退出 ,停止了。。。")
            return
        default:
            fmt.Println(name, "運行中。。。")
            time.Sleep(2 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go watch(ctx, "【任務1】")
    go watch(ctx, "【任務2】")
    go watch(ctx, "【任務3】")

    time.Sleep(time.Second * 10)
    fmt.Println("通知任務停止。。。。")
    cancel()
    time.Sleep(time.Second * 5)
    fmt.Println("真的停止了。。。")
}

運行結果如下:
在這裏插入圖片描述

參考博客

https://studygolang.com/articles/9624
https://www.cnblogs.com/qcrao-2018/archive/2019/06/12/11007503.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章