乐趣区

关于golang:Golang-一文带你深入context

前言

首先解答上一篇文章 一文带你疾速入门 context中留下的纳闷,为什么要defer cancelFunc()

func main() {parent := context.Background()
    for i := 0; i < 100; i++ {go doRequest(parent)
    }
    time.Sleep(time.Second * 10)
}

// doRequest 模仿网络申请
func doRequest(parent context.Context) {ctx, _ := context.WithTimeout(parent, time.Second*5)
    time.Sleep(time.Millisecond * 200)
    go func() {<-ctx.Done()
        fmt.Println("ctx done!")
    }()}

看下面的代码,在 main 函数中异步调用 doRequest 函数,doRequest函数中新建一个 5s 超时的上下文,doRequest函数的调用时长为200ms

<img src=”https://images-1255831004.cos.ap-guangzhou.myqcloud.com/halo/2021-07-10-142940.png” alt=”image-20210710222940035″ style=”zoom:50%;” />

能够看到,doRequest的上下文工夫范畴远大于函数调用破费的工夫,在函数完结后没有被动勾销上下文,这会造成上下文泄露

所以,defer cancelFunc()的目标是防止上下文泄露!!

被动调用 cancelFunc 是一个好习惯!

理解一下 Context 接口

type Context interface {// [1] 返回上下文的截止工夫
    Deadline() (deadline time.Time, ok bool)
  // 返回一个通道,当上下文完结时,会敞开该通道,此时 <-ctx.Done() 完结阻塞
  Done() <-chan struct{}
  // [2] 该办法会在上下文完结时返回一个 not nil err,该 err 用于示意上下文完结的起因
  Err() error
  // 返回与 key 关联的上下文的 value
  Value(key interface{}) interface{}}

[1]处,当上下文没有设置截止工夫时,调用Deadline,返回后果值中,ok = false

func main() {ctx, cancelFunc := context.WithCancel(context.Background())
    defer cancelFunc()
    deadline, ok := ctx.Deadline()
    fmt.Printf("ok = %v, deadline = %v\n", ok, deadline)
    // 输入 ok = false, deadline = 0001-01-01 00:00:00 +0000 UTC
}

[2]处 ,即便被动勾销上下文,Err 返回值not nil

func main() {
  // 设置上下文 10s 的超时工夫
    ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second * 10)
    go func() {
        // 1s 后被动勾销上下文
        <-time.After(time.Second)
        cancelFunc()}()
    <-ctx.Done()
    err := ctx.Err()
    fmt.Printf("err == nil ? %v\n", err == nil)
    // 输入 err == nil ? false
}

有几个构造体不能错过

看完 Context 接口后,咱们来理解一下 context 包中预约义的 4 种上下文 对应的构造体

<img src=”https://images-1255831004.cos.ap-guangzhou.myqcloud.com/halo/2021-07-10-151151.png” alt=”image-20210710231150954″ style=”zoom:50%;” />

能够看到,4 种上下文别离对应 3 种构造体,超时上下文和截止工夫上下文底层应用都是timerCtx

而后,来看看这 3 种构造体当中有什么属性,以及它们是如何实现 Context 接口

cancelCtx

type cancelCtx struct {Context // [1] 匿名接口

    mu       sync.Mutex            // 这个锁是用来爱护上面这些字段的
    done     chan struct{}         // [2] 这个 channel 的初始化形式为懒加载
    children map[canceler]struct{} 
    err      error                 
}

// 新建可勾销的上下文
func newCancelCtx(parent Context) cancelCtx {return cancelCtx{Context: parent}
}

func (c *cancelCtx) Value(key interface{}) interface{} {
    if key == &cancelCtxKey {return c}
  // 搜寻父级上下文的 value
    return c.Context.Value(key)
}

func (c *cancelCtx) Done() <-chan struct{} {c.mu.Lock()
    if c.done == nil {
    // 懒加载,第一次调用 Done 办法的时候,channel 才初始化
        c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
}

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

[1]处 ,能够看到cancelCtx 嵌入了一个匿名接口

构建 cancelCtx 构造体时,应用父级上下文 parent 作为构造体匿名接口的实现

同时 构造体中重写了匿名接口中的 3 个办法,别离是ValueDoneErr

所以,当调用 cancelCtx 中的 Deadline 办法时,实际上是调用 parentDeadline办法

[2]处 ,构造体中示意上下文完结的done 通道是懒加载的模式初始化,会在首次调用 Done 办法的时候,初始化 done 通道

timerCtx

type timerCtx struct {cancelCtx // [1] 内嵌构造体
    timer *time.Timer // [2] 用于实现截止工夫

    deadline time.Time
}

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

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
  // 构建超时上下文底层也是通过构建截止工夫上下文
    return WithDeadline(parent, time.Now().Add(timeout))
}

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // 当子上下文的截止工夫超过父级上下文时,间接结构可勾销的上下文并返回
        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(false, 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) }
}

[1]处 能够看到,timerCtx内嵌 cancelCtx 构造体,所以构建 timerCtx 时,也是承受父级上下文 parent 作为其内嵌接口的实现,而且 timerCtx 只重写 Deadline 办法

[2]处 能够看到,上下文的截止工夫的管制实质就是通过 timer 定时器管制,通过 timer.AfterFunc 实现在指定工夫 cancel 掉上下文

valueCtx

type valueCtx struct {
    Context
    key, val interface{}}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {return c.val}
    return c.Context.Value(key) // 寻找父级上下文中是否蕴含与该 key 关联的值
}

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {panic("nil key")
    }
  // [1] 存入 key 的类型是不可比拟时间接 panic
    if !reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

valueCtx整体还是前两个构造体更为简略,父级上下文 parent 只重写了 Value 办法

次要关注的中央是 [1] 处什么类型是不可比拟的?

  • slice
  • map
  • func

这三种类型是不能够比拟的,也就是 将切片、map 或者函数作为 valueCtxkey是会导致程序 panic 的!!

思考以下几个问题

上下文中的通道为什么要懒加载?

我的猜想是 节俭内存

首先,不论是被动勾销还是定时完结上下文,都会调用到 cancel 函数

函数中会判断,此时上下文的通道是否为空,如果为空,则应用一个全局变量 closedchan,这个通道是在包初始化阶段就close

// 这是一个可重复使用的通道
var closedchan = make(chan struct{})

func init() {
  // 包初始化时,敞开通道
    close(closedchan)
}

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
  ....
  if c.done == nil {
    // 如果 done 为空,就代表以后还没调用过 Done 办法,则间接应用 closechan 代替
        c.done = closedchan
    } else {close(c.done)
    }
  ....
}

上下文的应用不肯定都须要调用 Context.Done 办法

通过可重复使用的 closedchan,防止了在构建上下文的过程中立马初始化done 通道,缩小了一些不必要的内存调配

屡次调用 cancelFunc 会怎么样?

并不会怎么样,屡次被动勾销上下文不会产生任何谬误

调用 cancelFunc 时,底层调用 cancel 函数,函数中会判断以后上下文是否曾经完结,如果曾经完结了则间接return

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 不为空,代表上下文曾经被勾销掉了,间接完结流程
        c.mu.Unlock()
        return 
    }
  ...
}

<img src=”https://images-1255831004.cos.ap-guangzhou.myqcloud.com/halo/2021-07-10-164732.jpg” alt=”img” />

以后上下文的截止工夫是否超过父级上下文的截止工夫?

不能,此时上下文的截止工夫会跟父级上下文的截止工夫保持一致

能够看到,WithDeadline函数中,第一步就校验了截止工夫

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // 当子上下文的截止工夫超过父级上下文时,间接结构可勾销的上下文并返回
        return WithCancel(parent)
    }
  ....
}

当返回一个可勾销的上下文时,示意子上下文的截止工夫跟父级上下文是统一的

background 和 todo 的区别是什么?

实质上并没有任何区别 ,底层都是应用emptyCtx 结构的,次要的区别在于应用语义上

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

func Background() Context {return background}

func TODO() Context {return todo}

当不确定要传什么上下文的时候,就抉择TODO,不过通常这种状况都应该是暂时性的

退出移动版