golang中context包的理解

1 简介

golang 中的创建一个新的 goroutine , 并不会返回像c语言类似的pid,我们不能从外部杀死某个goroutine,只能让它自己结束,之前我们用channel+select 的方式来解决这个问题,但是有些场景实现起来比较麻烦,例如由一个请求衍生出的各个 goroutine 之间需要满足一定的约束关系,以实现一些诸如有效期,中止routine树,传递请求全局变量之类的功能。于是google 就为我们提供一个解决方案,开源了context包。

context包不仅实现了在程序单元之间共享状态变量的方法,同时能通过简单的方法,使我们在被调用程序单元的外部,通过设置ctx变量值,将过期或撤销这些信号传递给被调用的程序单元。在网络编程中,若存在A调用B的API, B再调用C的API,若A调用B取消,那也要取消B调用C,通过在A,B,C的API调用之间传递Context ,以及判断其状态,就能解决此问题,这是为什么gRPC的接口中带上ctx context.Context参数的原因之一。


2 源码剖析

2.1 核心接口context.Context

//  context 包里的方法是线程安全的,可以被多个 goroutine 使用    
type Context interface {               
    // 当Context 被 canceled 或是 times out 的时候,Done 返回一个被 closed 的channel      
    Done() <-chan struct{}        

    // 在 Done 的 channel被closed 后, Err 代表被关闭的原因   
    Err() error 

    // 如果存在,Deadline 返回Context将要关闭的时间  
    Deadline() (deadline time.Time, ok bool)

    // 如果存在,Value 返回与 key 相关了的值,不存在返回 nil  
    Value(key interface{}) interface{}
}

我们不需要手动实现这个接口,context 包已经给我们提供了两个,一个是 Background(),一个是 TODO(),这两个函数都会返回一个 Context 的实例。只是返回的这两个实例都是空 Context。


2.2 创建context后代的方法

// WithCancel对应的是cancelCtx,返回一个 cancelCtx和一个CancelFunc,CancelFunc是 context包中定义的一个函数类型:type CancelFunc func()。调用这个CancelFunc 时,关闭对应的c.done,也就是让他的后代goroutine退出。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// WithDeadline和WithTimeout对应的是timerCtx ,WithDeadline和WithTimeout是相似的,WithDeadline 是设置具体的deadline时间,到达deadline的时候,后代 goroutine退出,而WithTimeout简单粗暴,直接return WithDeadline(parent, time.Now().Add(timeout))。

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

// WithValue对应valueCtx,WithValue是在Context 中设置一个map,拿到这个Context以及它的后代的goroutine 都可以拿到map里的值。

func WithValue(parent Context, key interface{}, val interface{}) Context


2.3 context方法实现

cancelCtx结构体继承了Context,实现了canceler方法。valueCtx和timerCtx结构体继承了cancelCtx, 都实现了canceler接口。

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

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             // 当其被cancel时将会把err设置为非nil
}

func (c *cancelCtx) Done() <-chan struct{} {
    return c.done
}

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

//核心是关闭c.done
//同时会设置c.err = err, c.children = nil
//依次遍历c.children,每个child分别cancel
//如果设置了removeFromParent,则将c从其parent的children中删除
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 // already canceled
    }
    c.err = err
    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) // 从此处可以看到 cancelCtx的Context项是一个类似于parent的概念
    }
}

// timerCtx 结构继承 cancelCtx
type timerCtx struct {
    cancelCtx //此处的封装为了继承来自于cancelCtx的方法,cancelCtx.Context才是父亲节点的指针
    timer *time.Timer // Under cancelCtx.mu. 是一个计时器
    deadline time.Time
}

// valueCtx 结构继承 cancelCtx
type valueCtx struct {
    Context
    key, val interface{}
}


3 context使用

3.1 使用原则

context的创建和调用关系总是像层层调用进行的,就像人的辈分一样,为了实现这种关系,Context结构也应该像一棵树,叶子节点须总是由根节点衍生出来的。

一般创建context根节点使用context.Background(),函数返回空的Context,有了根节点,又该怎么创建其它的子节点、孙节点呢?使用WithCancel、WithDeadline、WithTimeout、WithValue这四个函数可以在根节点基础上创建子节点、孙节点……

一般使用原则:

  • 不要把Context 存在一个结构体当中,显式地传入函数。Context 变量需要作为第一个参数使用,一般命名为ctx(列如:func Add(ctx context.Context, a, b int) int)
  • 即使方法允许,也不要传入一个nil的Context ,如果你不确定你要用什么 Context的时候传一个context.TODO。
  • 使用context的Value 相关方法只应该用于传递和请求相关的元数据,不要用它来传递一些可选的参数. -同样的Context可以用来传递到不同的goroutine 中,Context 在多个goroutine中是安全的.


3.2 使用示例

package main

import (
    "fmt"
    "golang.org/x/net/context"
    "math/rand"
    "time"
)

// 模拟一个随机延时的阻塞函数
func randSleep(ctx context.Context) error {
    rand.Seed(time.Now().UnixNano())
    n := time.Duration(rand.Intn(5000))
    fmt.Printf("time sleep %d millisecond\n", n)

    select {
    case <-ctx.Done():
        return ctx.Err()
    case <-time.After(n * time.Millisecond):
    }

    return nil
}

// 求两个数的平方和,如果计算被中断,则返回 -1
func quadraticSum(ctx context.Context, a, b int) (int, error) {
    select {
    case <-ctx.Done():
        return -1, ctx.Err()
    default: // 处理
        err := randSleep(ctx)
        if err != nil {
            return -1, err
        }
    }

    return a*a + b*b, nil
}

// 定时取消执行
func autoCancel() {
    ctx, _ := context.WithTimeout(context.Background(), 2*time.Second) // 设置deadline时间为2秒

    a, b := 1, 2
    result, err := quadraticSum(ctx, a, b) // 当执行时间超过2秒,自动取消
    if err != nil {
        fmt.Printf("autoCancel error, %v\n", err)
    } else {
        fmt.Printf("autoCancel: %d*%d + %d*%d =  %d\n", a, a, b, b, result)
    }

}

// 手动取消
func manualCancel() {
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        time.Sleep(3 * time.Second)
        cancel() // 3秒后主动取消
    }()

    a, b := 3, 4
    result, err := quadraticSum(ctx, a, b)
    if err != nil {
        fmt.Printf("manualCancel error, %v\n", err)
    } else {
        fmt.Printf("manualCancel: %d*%d + %d*%d =  %d\n", a, a, b, b, result)
    }
}

func main() {
    go autoCancel()
    time.Sleep(10 * time.Millisecond)
    go manualCancel()

    time.Sleep(10 * time.Second)
}


参考:
https://studygolang.com/articles/9517 https://www.cnblogs.com/DaBing0806/p/6878810.html



专题「golang相关」的其它文章 »