写点什么

golang context 详解

作者:en
  • 2022 年 2 月 17 日
  • 本文字数:3173 字

    阅读完需:约 10 分钟

golang context详解

一.前言

之前写过一篇 context 的源码学习 context 源码学习 ,写完还觉得,嗯,我终于弄懂了 context,但是最近在面试,别人一问我只能说个大概,到底还是理解不深,只是看了源码,没有往深层去想,他为什么要这么设计。

(坦率的说个人认为面试蛮看缘分,有时候对方问的你恰好了解就撞上了,有时候对方问的你在日常中没有用到,就确实不懂,但是作为面试官是不会故意问一下冷僻的东西的,所以可能面试官问的就是他们研发有用到的东西,那我虽然在当前工作中没有这个场景,但是多学习留个印象,总是好的。)

二.正文

2.1 context 方法

type Context interface {	Deadline() (deadline time.Time, ok bool)	Done() <-chan struct{}	Err() error	Value(key interface{}) interface{}}
复制代码

先来看 context 的接口,里面有 4 个方法,

Deadline():deadline 返回 ctx 完成工作应该被取消的时间,当 deadline 没有被设置返回 ok==false

Done():返回一个代表词 context 工作完成的管道(channel 这个也可以单开一篇),一般与 select 一起使用


//stream一直做某件事情,直到某件事情报错或者ctx被关闭func Stream(ctx context.Context, out chan<- Value) error {	  	for {	  		v, err := DoSomething(ctx)	  		if err != nil {	  			return err	  		}	  		select {	  		case <-ctx.Done():	  			return ctx.Err()	  		case out <- v:	  		}	  	}  }
复制代码

Err():若 done 没有关闭则 err 返回空,若 done closed(被 cancel 或者 deadline 超时),则 err 返回非空解释

Value():用于存储和上下文关联的 key

以上是 context 的四个基本方法,通过看源码能知晓 context 最重要的两个功能,一个是 cancel,一个是 deadline,接下来分别探究一下。

2.2 cancelCtx

2.2.1 cancelCtx 结构体

//canceler指的是一种可以直接取消的上下文type canceler interface {	cancel(removeFromParent bool, err error)	Done() <-chan struct{}}
//可以取消的上下文,取消的时候还会将所有由该上下文派生的的子上下文一并取消type cancelCtx struct { Context //父亲上下文 mu sync.Mutex // 保护并发 done atomic.Value //Value 提供一致类型值的原子加载和存储。Value 的零值从 Load 返回 nil。调用 Store 后,不得复制 Value。第一次使用后不得复制 Value。 children map[canceler]struct{} // 存储此上下文下的子上下文 err error // set to non-nil by the first cancel call}
复制代码

先从结构看起,cancelCtx 的核心是可以主动取消的上下文,他取消的时候还会将所有由该上下文派生的的子上下文一并取消,那我们就要思考,他是如何做到的呢?

mu:锁,用于保护并发 ,首先根据 cancelCtx 的核心需求,可以取消派生的所有上下文,也就意味着我们需要存储这个 context 派生的所有子 context,那我们推测这个锁的作用应该就是保护存储子上下文或者删除子上下文的结构体。(总结会含有真正作用)

done推测用于标识此结构体是否结束。

children:是一个 canceler 的 map,可以发现就是用于存储上下文的结构题

err:错误信息,用于判断是否已经取消

2.2.2 cancelCtx 函数实现

2.2.2.1 Done()函数

//主要用于发送结束信号//获取done的管道,若非空直接返回,若为空//加锁->再次查询->若非空返回->为空则新建一个管道,将其存储->函数结束通过defer特性释放锁func (c *cancelCtx) Done() <-chan struct{} {	d := c.done.Load()	if d != nil {		return d.(chan struct{})	}	c.mu.Lock()	defer c.mu.Unlock()	d = c.done.Load()	if d == nil {		d = make(chan struct{})		c.done.Store(d)	}	return d.(chan struct{})}
复制代码

2.2.2.2 Err()函数

//过于简单,不多赘述func (c *cancelCtx) Err() error {	c.mu.Lock()	err := c.err	c.mu.Unlock()	return err}
复制代码

2.2.2.3 cancel()函数

//应用//带cancel功能的context是可嵌套的//a作为b,c的父级//b,c可以派生自己的子级//当父context取消的同时会取消所有由父context派生的子context//逻辑//加锁-->若c.err非空说明已经取消,直接返回-->若c内的done为空,将一个全局的可重用关闭管道赋值-->//若c内done非空将其关闭-->遍历所有子上下文,将他们都关闭-->从父上下文移除自身-->通过defer特性进行锁关闭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	d, _ := c.done.Load().(chan struct{})	if d == nil {		c.done.Store(closedchan)	} else {		close(d)	}	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) }}
复制代码

2.2.3 cancelCtx 总结

为什么需要 Context 和 children?

因为 cancelContext 是父子相关联的,一个 cancelContext 取消的时候既需要干掉自己所有的子,也需要告诉自己的父。

为什么需要 mu?

context 被多个协程互相传递使用,这就要保证它一定要是并发安全的,实现过程中各种修改操作,如取消,删除子,增加子,都需要用锁保证并发安全。

使用例子

func CloseContext(ctx context.Context){	for {		select {    //监听上下文取消		case <-ctx.Done():			fmt.Println(ctx.Err())			return		}	}}
func main() { //父上下文 ctx:=context.Background() //构造子的可cancel的上下文 cancelctx,cancel:=context.WithCancel(ctx) go CloseContext(cancelctx) //主动取消 cancel() //防止主协程先于子退出 time.Sleep(1*time.Second)}
复制代码

2.3 timerCtx

2.3.1 timerCtx 结构体

type timerCtx struct {	cancelCtx	timer *time.Timer // Under cancelCtx.mu.	deadline time.Time}
复制代码

timerCtx 的核心是在到达指定时间后自动 cancel,所以相对于 cancelCtx 它只新增了两个结构体

timer:计时器

deadline:截止时间

2.3.2 timerCtx 函数实现

2.3.2.1 cancel()函数

func (c *timerCtx) cancel(removeFromParent bool, err error) {	//取消cancel context  c.cancelCtx.cancel(false, err)	if removeFromParent {		// Remove this timerCtx from its parent cancelCtx's children.		removeChild(c.cancelCtx.Context, c)	}	c.mu.Lock()  //由于已经取消了所以停止计时	if c.timer != nil {		c.timer.Stop()		c.timer = nil	}	c.mu.Unlock()}
复制代码

2.3.2.2 Deadline()函数

特色,返回超时的截止时间

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

2.3.3 timerCtx 总结

为什么需要 timer 和 deadline 的理由十分明显,就只贴上例子代码了

func CloseContext(ctx context.Context){	for {		select {		case <-ctx.Done():			fmt.Println(ctx.Err())			return		}	}}
func main() { //父上下文 ctx:=context.Background() //构造子的可cancel的上下文 cancelctx,_:=context.WithCancel(ctx) //构造一个5秒后子冻销毁的上下文 deadlineContext,_:=context.WithDeadline(cancelctx,time.Now().Add(5*time.Second)) go CloseContext(deadlineContext) //防止主协程先于子退出 time.Sleep(10*time.Second)}
复制代码

三.参考

并没有参考什么,除了源码,例子也是我自己写的,整体就是带着问题再去看了一遍 context 的源码,并思考了一下为什么这么设计,以及结构体里面各个字段的必要性。

纵隔我自己觉得已经把 context 的实现原理吃透了,但是由于生产中只用来做调用链追踪,如果之后遇到更多场景会实时追加~

用户头像

en

关注

努力分享对他人有价值的知识 2018.06.14 加入

还未添加个人简介

评论

发布
暂无评论
golang context详解