Golang context 万字解析实现原理

Context 是什么

  1. 译为上下文
  2. 用于进程之间信息和信号的传递
  3. 用于服务之间的信号和信息从传递

Context 的功能

  1. Context 可以用于不同的api或者进程之间传递(携带键值对传递),不要传递 nil Context
  2. 传递取消信号(主动取消,超时/时限取消),因为Context是树结构,所以传递是单向传递的,只有父节点取消的时候,才会把取消的信号传递给父节点的衍生子节点
  3. Context需要显示传递给他所需要的函数,并且需要他的函数作为第一个参数,通常命名为ctx

应用场景

  1. 用于父子协程间取消信号传递
  2. 用于客服端与服务器之间的信息传递
  3. 用于设置请求超时时间等

源码分析

1.10 Context核心结构

   本文都是围绕一下进行讨论的,无非就是实现这四个方法的逻辑不同。方法的具体实现,将会在下面具体讲解,现在先让大家对Context有个宏观的认知

  • 一个接口
type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}
  1. Deadline(): 返回一个完成工作的截止时间,这个截止时间代表上下文被取消的时间,这个时间之后所有的衍生协程都将会取消。如果没有截止时间,将会返回false
  2. Done() :返回一个Context中的channel,这个channel会在当前任务完成工作的时候关闭,如果关闭的时候无法取消上下文,则Done可能返回nil。多次调用Done方法会返回同一个channel。
  3. Err() :返回Context结束的原因,他只会在Done方法对应的Channel关闭的时候关闭返回非空值,如果context被取消,会返回context.Canceled如果Context超时,会返回Context.DeadlineExceeded错误
  4. Value():从Context从获取对应的键值对,如果未设置键值对则返回nil。相同的key取value会返回相同的值。因为这四个方法都是幂等
  • 四个实现
  1. emptyCtx (): 实现了一个空的context用作根节点,TODO(),Background(),都是基于他实现的
  2. cancelCtx (): 实现一个能够自己取消的Context
  3. timerCtx (): 实现一个通过定时器timer和截止时间deadline定时取消的context
  4. valueCtx (): 实现一个可以通过 key、val 两个字段来存数据的context
  • 十个方法
  1. Background (): Background 返回一个emptyCtx作为根节点

  2. TODO (): TODO 返回一个emptyCtx作为未知节点,如果你不知道使用哪个Context可以使用此方法

  3. WithCancel(): WithCancel 入参是一个父Context,return一个子Context和cancelCtx(主动取消Context),如果想取消Context可以调用cancelCtx()

  4. WithDeadline (): WithDeadline 入参是一个父Context和一个过期时间, 返回一个子Context和timerCtx,如果时间超时,那么他会自动的调用timerCtx()方法

  5. WithTimeout (): WithTimeout 入参是一个父Context和一段时间, 返回一个子Context和timerCtx,如果传入的是3秒,那么3秒后,他会自动的调用timerCtx()方法

  6. WithCancelCause(): WithCancelCause,和WithCancel不同点就是他可以自定义error而WithCancel不能

  7. WithDeadlineCause(): 同上

  8. WithTimeoutCause(): 同上

  9. AfterFunc(): AfterFunc入参是一个Context和一个func(),在该Context取消的时候,会执行该func。可以解决以往的一些合并取消上下文和串联处理的复杂场景。AfterFunc 安排在 ctx 完成后在自己的 goroutine 中调用 f(取消或超时)。如果 ctx 已经完成,AfterFunc 会立即在自己的 goroutine 中调用 f。

  10. WithoutCancel(): WithoutCance返回一个父级Context的副本,即使父Context关闭,生成的父Contest的副本也不会关闭。

1.1Err错误

   从下面的err可以看出,有两个错误方法,Canceled:Context取消时返回的err,DeadlineExceeded :Context截止时间超时的err

// Canceled is the error returned by [Context.Err] when the context is canceled.
var Canceled = errors.New("context canceled")

// DeadlineExceeded is the error returned by [Context.Err] when the context's
// deadline passes.
var DeadlineExceeded error = deadlineExceededError{}

type deadlineExceededError struct{}

func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }
func (deadlineExceededError) Temporary() bool { return true }

2 .1 emptyCtx

   字如其名就是一个空的Context,无法被取消没有值也没有超时时间

// An emptyCtx is never canceled, has no values, and has no deadline.
// It is the common base of backgroundCtx and todoCtx.
type emptyCtx struct{}

func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
	return
}

func (emptyCtx) Done() <-chan struct{} {
	return nil
}

func (emptyCtx) Err() error {
	return nil
}

func (emptyCtx) Value(key any) any {
	return nil
}
  1. Deadline(): 方法会返回一个公元元年的时间以及false的flag。表示这个Context没有超时时间
  2. Done() : 方法返回一个nil,如果用户读取或者写入这个Channel,会造成阻塞
  3. Err(: Err的错误返回一个nil,证明这个方法不会有err
  4. Value: Value值返回nil,证明这个方法不会有value

3.1 Background() 和 TODO()

   从源码可以看出这两个方法其实都是返回的emptyCtx 的一个实例,只是他们的表达方式不一样,他们两个继承了emptyCtx 所有的特点,,无法被取消没有值也没有超时时间

   Background():上下文的默认值,相当于root,所有的Context都应该从这里衍生出来,一般会在main中作为最顶层的Context

   TODO():Todo就是表示这个地方还没有完成,代表目前还不知道用哪个Context传递,或者根本没有Context,但是业务又需要传递Context,这时TODO就派上用场了

type backgroundCtx struct{ emptyCtx }

func (backgroundCtx) String() string {
	return "context.Background"
}

type todoCtx struct{ emptyCtx }

func (todoCtx) String() string {
	return "context.TODO"
}

func Background() Context {
	return backgroundCtx{}
}
func TODO() Context {
	return todoCtx{}
}

4.1.1 cancelCtx

   cancelCtx 是一个能够被取消的Context,其中他实现了Context接口,他是一个匿名字段,可以看成Context.

type canceler interface {
	cancel(removeFromParent bool, err, cause error)
	Done() <-chan struct{}
}
type cancelCtx struct {
	Context
	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
	cause    error                 // set to non-nil by the first cancel call
}
  • Context:嵌入了一个Context作为父Context,表示cancelCtx 肯定是某一个Context的子节点

  • mu :互斥锁,用来保证并发安全

  • done:实际的类型是 chan struct{},用来反应cancelCtx 生命周期的通道,如果是cancelCtx 关闭,那么就会向done发送信号,用来表示传递关闭信号。其中内部是使用atomic.Value实现的,会读取上次最近存储的chan struct{}

  • children:指向 cancelCtx 的所有子 context,在第一次调用的时候为nil

  • err:记录了当前 cancelCtx 的错误.

  • cause:自定义err,返回自定义的cancelCtx 错误

4.1.2 Deadline 方法

   cancelCtx 未实现该方法,仅是 嵌入了一个带有 Deadline 方法的 Context interface,因此倘若直接调用会报错.

4.1.2 Done 方法

  Done返回一个只读的通道,当Context取消的时候,该通道就会关闭,你可以监听这个通道检测Context是否被关闭,将done初始化。

  • 检查atomic.Value 是否存储了chan struct{},如果存储的有就返回原来的实例化的chan struct{}
  • 如果atomic.Value没有存储,就加锁,然后在判断一下是否有没有存储(双重保险),没有了在进行实例化,最后返回实例化后的chan struct{}
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{})
}

4.1.2 Err 方法

   这个就是读取cancelCtx的err,然后进行返回

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

4.1.2 Value 方法

倘若 key 特定值 &cancelCtxKey,则返回 cancelCtx 自身的指针,否则就会取值return

func (c *cancelCtx) Value(key any) any {
	if key == &cancelCtxKey {
		return c
	}
	return value(c.Context, key)
}

了解了cancelCtx 结构之后进入真正的主题

4.2 WithCancel() 和 WithCancelCause()

   WithCancel():返回的是一个继承了父Context的新Context(cancelCtx),和一个CancelFunc方法,如果调用CancelFunc,这个context 将会取消。

   WithCancelCause():返回的内容一样,就是CancelFunc方法多了一个自定义的err。下面是使用示例,myError是自定义的err。

	ctx, cancel := context.WithCancelCause(parent)
	cancel(myError) 
	//返回myError
	context.Cause(ctx) 
type CancelFunc func()

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := withCancel(parent)
	return c, func() { c.cancel(true, Canceled, nil) }
}

type CancelCauseFunc func(cause error)

func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
	c := withCancel(parent)
	return c, func(cause error) { c.cancel(true, Canceled, cause) }
}

func withCancel(parent Context) *cancelCtx {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := &cancelCtx{}
	c.propagateCancel(parent, c)
	return c
}
  • 先判断父Context是否为nil,如果为nil直接panic,然后在重新实例化一个子context
  • 在 propagateCancel 方法内启动一个守护协程,以保证父 context 终止时,该 cancelCtx 也会被终止

4.2.2 propagateCancel

   如果父context取消了,那么他的所有衍生context都应该被取消

func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
	c.Context = parent

	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err(), Cause(parent))
		return
	default:
	}

	if p, ok := parentCancelCtx(parent); ok {
		// parent is a *cancelCtx, or derives from one.
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err, p.cause)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
		return
	}

	if a, ok := parent.(afterFuncer); ok {
		// parent implements an AfterFunc method.
		c.mu.Lock()
		stop := a.AfterFunc(func() {
			child.cancel(false, parent.Err(), Cause(parent))
		})
		c.Context = stopCtx{
			Context: parent,
			stop:    stop,
		}
		c.mu.Unlock()
		return
	}

	goroutines.Add(1)
	go func() {
		select {
		case <-parent.Done():
			child.cancel(false, parent.Err(), Cause(parent))
		case <-child.Done():
		}
	}()
}
  • c.Context = parent,把该context指向父context,这样就知道父context有哪些子context了

  • 判断父context是否为不可取消类型,如果是,就跳过,如果不是就监听父context是否已经取消了,如果是取消了就把该子context取消。并把父err,传递给子err

  • 如果parent 是 cancelCtx 的类型,则加锁,并将子 context 添加到 parent 的 children map 当中,假如以后父context取消,可以直接用children map 取消所有的子context

  • 如果parent 实现了afterFuncer接口,把cancel函数封装成一个stop函数,进行延迟取消。赋值给子context的结构体中。后续会调用这个afterFuncer

  • 如果 parent 没有实现afterFuncer接口,则启动一个协程,通过多路复用的方式监控 parent 状态,如果终止,则同时终止子 context,并把parent 的 err传递给子context,如果孩子终止则直接跳过,因为不影响parent 。这里启动协程是主要监听parent,如果parent取消则立即取消子context。

4.2.3 cancelCtx.cancel()

   在propagateCancel守护协程中,知道了怎么终止协程cancel(false, parent.Err(), Cause(parent))。但是你知道怎么实现的吗?来看源代码

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	if cause == nil {
		cause = err
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	c.cause = cause
	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, cause)
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}
  • 可以看到cancel需要三个入参,一个是是否需要从父map[canceler]struct{}剔除,如果为true就剔除。第二个是取消context的错误是必传的,第三个是自定义err。
  • 如果err为nil直接panic,检查cancelCtx的err是否已经为空,如果不为空则解锁return,为空就把入参赋值给err和cause
  • 加载cancelCtx 的 channel,如果没有初始化,则初始化一个closedchan,反之就关闭Channel。遍历当前cancelCtx的children 把他的衍生孩子都进行取消。然后把children 清空赋值为nil。
  • 下面是removeChild的方法,先判断parent是否实现了stopCtx结构体,如果实现了就证明它实现了afterFunction这个接口,需要在移除的时候,运行在propagateCancel中添加的stopCtx.stop方法。
  • 如果parent不是cancelCtx,那么就返回,只有cancelCtx才有children ,如果是cancelCtx类型,就把children map中的child 删除,最后解锁返回
func removeChild(parent Context, child canceler) {
	if s, ok := parent.(stopCtx); ok {
		s.stop()
		return
	}
	p, ok := parentCancelCtx(parent)
	if !ok {
		return
	}
	p.mu.Lock()
	if p.children != nil {
		delete(p.children, child)
	}
	p.mu.Unlock()
}
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

4.3 WithTimeout (),WithDeadline(),WithDeadlineCause(),WithTimeoutCause()

因为这四个方法实现的逻辑都基本是一样的,下面将会讲解怎么实现具体逻辑

4.3.1 timerCtx 结构

timerCtx 在 cancelCtx 基础上又做了一层封装,他继承了cancelCtx结构体,实现了cancelCtx的所有能力,然后外加了一个定时取消的Context,和到截止时间取消的Context

type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

4.3.2 Deadline结构

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

在之前讲解的 WithCancel() 和 WithCancelCause()中,都没有实现Deadline()这个方法,这个方法用于展示过期时间

4.3.3 cancel结构

因为他继承了cancelCtx,所以可以之间调用cancelCtx.cancel,进行取消操作,然后判断是否需要把孩子context从map中删除,然后停止timer

func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
	c.cancelCtx.cancel(false, err, cause)
	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()
}

4.3.4 String结构

这个是把context进行name的拼接,调用这个方法会把context的名字return

func (c *timerCtx) String() string {
	return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
		c.deadline.String() + " [" +
		time.Until(c.deadline).String() + "])"
}

4.4.1 WithDeadline 结构

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	return WithDeadlineCause(parent, d, nil)
}

// WithDeadlineCause behaves like [WithDeadline] but also sets the cause of the
// returned Context when the deadline is exceeded. The returned [CancelFunc] does
// not set the cause.
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
	c := &timerCtx{
		deadline: d,
	}
	c.cancelCtx.propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
		return c, func() { c.cancel(false, Canceled, nil) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded, cause)
		})
	}
	return c, func() { c.cancel(true, Canceled, nil) }
}
  • 从代码可以看到WithDeadline其实内部就是调用的WithDeadlineCause,只不过他的自定义err为nil。
  • 检查parent 是否为nil,校验 parent 的过期时间是否早于自己,如果是就返回 WithCancel(parent),然后构造一个timerCtx,启用守护进程,判断过期的时间是否已经到了,如果到则取消context,判断context的err是否为nil,如果为nil则设定一个延时时间,到过期时间的时候会终止该 timerCtx,并返回 的错误。最后返回一个timerCtx和cancel函数
  • 26
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Go 的学习之路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值