0
点赞
收藏
分享

微信扫一扫

go context详解

三千筱夜 2022-02-20 阅读 94
golang

一.前言

之前写过一篇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的实现原理吃透了,但是由于生产中只用来做调用链追踪,如果之后遇到更多场景会实时追加~

四.todo

1.golang channel

2.golang lock

举报

相关推荐

0 条评论