Golang中context笔记

一. context定义

context是go并发编程中经常用到的编程模式, 旨在实现以下机制:

  • 对于并发的层层任务, 做到上层任务取消之后, 所有的下层任务都会被取消
  • 中间的某层任务取消后, 只会将当前任务和其下层任务取消, 而不会影响到上层任务以及同级任务

注解: 我们将进程之间的串行抽象为一棵树, 那么每个进程就相当于树的节点, context就是每个节点对应的标识, 可以用于附带某些信息

二. context包中的类与函数

1. context接口

type Context interface {

    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}
}
  • Deadline()

Time类是time包下的类, 用来保存和传递时间, 可以使用Before/After/Equal来进行比较

无参, 返回一个Time类, 以及一个bool
这里返回的Time是绑定当前context的任务被取消的截止时间, 如果没有设定截止时间, 就会返回ok == false

  • Done()

无参, 返回一个只读的管道, 当绑定当前的context的任务被取消时, 返回一个关闭的channel管道, 否则返回一个nil
一般在select-case中使用

  • Err()

如果Done()返回的channel没有关闭, 将返回nil, 否则返回非空的error代表任务结束的原因

比如取消:Canceled, 超时: DeadlineExceeded

  • Value(key interface{}) interface{}

返回context存储的键值对中当前key 对应的 value, 如果没有的话返回nil


注解: 这里或许有点懵, 但是暂且先记住, 看到下面的接口实现, 自然会明白这些抽象方法的作用

2. emptyCtx 变量

emptyCtx是一个struct{}空结构体变量,但实现了context的接口。emptyCtx没有超时时间,不能取消,也不能存储任何额外信息,所以emptyCtx用来作为context树的根节点。

// 可以看出是空结构体
type emptyCtx struct{}

// 实现了Context接口
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
}

// 两种特殊的emptyCtx, 设置为私有
type backgroundCtx struct{ emptyCtx }

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

type todoCtx struct{ emptyCtx }

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

// 提供get方法
func Background() Context {
	return backgroundCtx{}
}

func TODO() Context {
	return todoCtx{}
}

但我们一般不会直接使用emptyCtx,而是使用由emptyCtx实例化的两个变量,分别可以通过调用BackgroundTODO方法得到,但这两个context在实现上是一样的。那么BackgroundTODO方法得到的context有什么区别呢?

Background 一般用于主函数, 初始化, 或者测试中, 作为context树的根节点
TODO一般用作不确定使用什么类型的context的时候才会使用, (下面会讲context的种类)

总而言之, 在下面的不同的context种类被创建时一般都会传入一个父节点, 我们会根据场景来选择background或者todo

3. cancelCtx

3.1 canceler

首先定义一个接口canceler

type canceler interface {
	cancel(removeFromParent bool, err, cause error)
	Done() <-chan struct{}
}
  • cancel(removeFromParent bool, err, cause error)

传入一个bool值, 一个error, 前者代表是否删除本身这个context, 后者就是删除的原因
如果实现了该接口, 就说明是可以取消的

3.2 cancelCtx
type cancelCtx struct {
	Context                        //嵌入接口类型,cancelCtx 必然为某个context的子context;

	mu       sync.Mutex            // 互斥锁,保护以下字段不受并发访问的影响
	done     atomic.Value          // 原子通道,第一次调用取消函数时被惰性创建,在该context及其后代context都被取消时关闭
	children map[canceler]struct{} // 其实是一个set, 保存当前context的所有子context, 也就是一个节点的子节点, 第一次取消调用时设为nil
	err      error                 // 第一次取消时设置为取消的错误原因
	cause    error                 // 同上
}
  • Done() 方法
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{})
}

c.done 使用了惰性加载机制, 只有第一次调用Done() 方法时才会被创建, 且通过加锁二次检查, 保证了多个goroutine同时调用Done() 方法时, 只有第一个可以创建管道
函数返回一个只读的channel, 当其关闭时, 就会立即读出零值, 据此可以判断cancelCtx是否被取消

  • Err() 方法
func (c *cancelCtx) Err() error {
	c.mu.Lock()
	err := c.err
	c.mu.Unlock()
	return err
}
  • Value()方法
func (c *cancelCtx) Value(key any) any {
	if key == &cancelCtxKey {
		return c
	}
	return value(c.Context, key)
}

当key = &cancelCtxKey时, 就会返回他本身, 否则就会一直往上找

3.3 WithCancel() 函数

可以创建一个cacelCtx

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

func withCancel(parent Context) *cancelCtx {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := &cancelCtx{}
	c.propagateCancel(parent, c)
	return c
}

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)
	}
}

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():
		}
	}()
}

这里主要聚焦四个函数
cancel方法, 绑定到cancelCtx指针上, 代表实现了canceler接口, 作用是取消这个节点, 设置上err和cause, 同时传入的bool代表是否删除该节点, 方法内还关闭了管道, 然后继续向子context传递取消信息

propagateCancel() 方法, 用以传递父子 context 之间的 cancel 事件,向上寻找可以挂靠可取消的context,并且“挂靠”上去。这样,调用上层 cancel 方法的时候,就可以层层传递,将那些挂靠的子 context 同时“取消”。

withCancel() 私有方法, 创建一个cancelCtx{}, 然后返回, 同时调整当前的context, 向上寻找可以挂靠可取消的context
WithCancel() 向外提供的方法, 创建一个cancelCtx{}, 同时提供一个function, 用来取消当前的context

若 parent 是不会被 cancel 的类型(如 emptyCtx),不需要传递,则直接返回;
若 parent 已经被 cancel,则直接终止子 context,并以 parent 的 err 作为子 context 的 err;
若 parent 是 cancelCtx 的类型,则加锁,并将子 context 添加到 parent 的 children map 当中;
若 parent 不是 cancelCtx 类型,但又存在 cancel 的能力(比如用户自定义实现的 context),则启动一个协程,通过多路复用的方式监控 parent 状态,倘若其终止,则同时终止子 context,并传递 parent 的 err

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

也是创建一个cancelCtx, 同时返回一个function用于取消, 但是这里取消的时候可以返回一个错误原因, 比较好用

4. timerCtx

timerCtxcancelCtx 基础上又做了一层封装,除了继承 cancelCtx 的能力之外,新增了一个 time.Timer 用于定时终止 context;另外新增了一个 deadline 字段用于字段 timerCtx 的过期时间.

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

	deadline time.Time
}
4.1 继承方法
  • Deadline()
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
	return c.deadline, true
}

可见就是将其deadline返回

  • cancel(removeFromParent bool, err, cause error)
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()
}

直接使用父类的cancel方法, 然后判断是否需要从父亲的儿子列表中移除, 当都不需要的时候, 加锁, 将计时器停止并置空, 然后解锁返回

4.2 WithDeadlineCause
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,
	}
        //找到上一个可以取消的context, 接上去
	c.cancelCtx.propagateCancel(parent, c)
        // 计算一下现在到截止时间还有多少时间, 如果小于等于0说明已经到达截止时间, 就要直接cancel掉
	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) }
}

context包一共提供了四种创建的方法

WithDeadlineCause, WithDeadline, WithTimeout, WithTimeoutCause
如果带Cause就是带有结束原因
这四种创建方式都是对WithDeadlineCause的复用

5. ValueCtx

context的一种, 可以存储信息

type valueCtx struct {
	Context
	key, val any
}

func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, key)
}

func value(c Context, key any) any {
	for {
		switch ctx := c.(type) {
		case *valueCtx:
			if key == ctx.key {
				return ctx.val
			}
			c = ctx.Context
		case *cancelCtx:
			if key == &cancelCtxKey {
				return c
			}
			c = ctx.Context
		case withoutCancelCtx:
			if key == &cancelCtxKey {
				// This implements Cause(ctx) == nil
				// when ctx is created using WithoutCancel.
				return nil
			}
			c = ctx.c
		case *timerCtx:
			if key == &cancelCtxKey {
				return &ctx.cancelCtx
			}
			c = ctx.Context
		case backgroundCtx, todoCtx:
			return nil
		default:
			return c.Value(key)
		}
	}
}
  1. 可以看出实现了context接口, 并且这是个私有类
  2. 实现了Value方法, 用以在context链路上获取val. 如果当前context上不存在需要的key, 就会一直沿着父亲向上搜索, 当然如果搜索到根节点background还是没有, 那么就直接返回nil(可以看emptyCtx实现的Value方法), 当然如果遇到对应的key, 就会返回查找到的val
    所以可以看出, 这是一个递归的查找问题, 不断地跳父亲节点
  3. 其中 cancelCtx、timerCtx、emptyCtx 类型会有特殊的处理方式,如当前key== &cancelCtxKey ,则会返回cancelCtx自身,而emptyCtx 则会返回空
5.1 常用函数
  • WithValue()函数

用以向context添加键值对

func WithValue(parent Context, key, val any) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

传入一个父节点context, 然后对应的key和val, 在判断完key是否为空以及是否可比较之后, 就会创建一个valueCtx对象, 相当于连接到传入的context后面
这就是一个链式结构

三.总结

  1. 这里只是列举了常用的一些context类, 其实还有很多
  2. 不要在结构类型中加入 Context 参数,而是将它显式地传递给需要它的每个函数,并且它应该是第一个参数,通常命名为 ctx:
  3. Context 是线程安全的,可以放心地在多个 goroutine 中使用。
  4. 当你把 Context 传递给多个 goroutine 使用时,只要执行一次 cancel 操作,所有的 goroutine 就可以收到 取消的信号
  5. 不要把原本可以由函数参数来传递的变量,交给 ContextValue 来传递。
  6. 当一个函数需要接收一个 Context 时,但是此时你还不知道要传递什么 Context 时,可以先用 context.TODO 来代替,而不要选择传递一个 nil
  7. 当一个 Contextcancel 时,继承自该 Context 的所有 子 Context 都会被 cancel
posted @ 2023-12-20 14:57  Xingon2356  阅读(89)  评论(0)    收藏  举报