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,不过通常这种情况都应该是暂时性的

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 前言 Go1.7引入了context包,其中定义了多种上下文,包括可主动取消的上下文,带截止时间或超时时间的上下文...
    灭BUG阅读 207评论 0 0
  • 控制并发有两种经典的方式,一种是WaitGroup,另外一种就是Context,今天我就谈谈Context。 什么...
    豆瓣奶茶阅读 5,052评论 4 13
  • context包专门用来简化处理单个请求的多个goroutine之间与请求域的数据、取消信号、截止时间等相关操作。...
    wz998阅读 3,723评论 0 3
  • 参考Go语言实战笔记(二十)| Go ContextGolang context初探 一、WaitGroup 这是...
    合肥黑阅读 582评论 0 10
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,060评论 2 7