前言
首先解答上一篇文章一文带你疾速入门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个办法,别离是Value
,Done
,Err
所以,当调用cancelCtx
中的Deadline
办法时,实际上是调用parent
的Deadline
办法
[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或者函数作为valueCtx
的key
是会导致程序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
,不过通常这种状况都应该是暂时性的