6.1 Context

Kesa...大约 5 分钟golang

context.Contextopen in new window接口定义了四个方法:

  1. Deadline : 返回 context.Contextopen in new window 被取消的时间,即完成工作的截止时间
  2. Done :返回一个 Channel, 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel
  3. Err : 返回 context.Contextopen in new window 结束的原因,只会在 Done 方法对应的 Channel 关闭时返回非空的值:
    1. context.Contextopen in new window 被取消,返回 Canceled 错误
    2. context.Contextopen in new window 超时,返回 DeadlineExceeded 错误
  4. Value :从 context.Contextopen in new window 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据
type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

6.1.1 设计原理

context.Contextopen in new window最大作用是 在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费

golang-context-usage
golang-context-usage

context.Contextopen in new window 的作用是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期。

每一个 context.Contextopen in new window 都会从最顶层的 Goroutine 一层一层传递到最下层。context.Contextopen in new window 可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。

golang-without-context
golang-without-context

当最上层的 Goroutine 因为某些原因执行失败时,下层的 Goroutine 由于没有接收到这个信号所以会继续工作。

当使用 context.Contextopen in new window 时,就可以在下层及时停掉无用的工作以减少额外资源的消耗:

golang-with-context
golang-with-context

6.1.2 默认上下文

context.Backgroundopen in new windowcontext.TODOopen in new window都会返回预先初始化好的私有变量 backgroundtodo

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

这两个私有变量都是通过 new(emptyCtx) 语句初始化,是指向私有结构体 context.emptyCtxopen in new window 的指针:

type emptyCtx int

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 interface{}) interface{} {
	return nil
}

context.emptyCtxopen in new window 通过空方法实现了 context.Contextopen in new window 接口中的所有方法,没有任何功能。

golang-context-hierarchy
golang-context-hierarchy

context.Backgroundopen in new windowcontext.TODOopen in new window 互为别名,没有太大的差别,只是在使用和语义上不同:

多数情况下,若当前函数没有上下文作为入参,会使用 context.Backgroundopen in new window 作为起始的上下文向下传递。

6.1.3 取消信号

WithCancel

context.WithCancelopen in new window 函数能够从 context.Contextopen in new window衍生出一个新的Context并返回用于取消该上下文的函数。

golang-parent-cancel-context
golang-parent-cancel-context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}
func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
	if done == nil {
		return // 父上下文不会触发取消信号
	}
	select {
	case <-done:
		child.cancel(false, parent.Err()) // 父上下文已经被取消
		return
	default:
	}

	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			child.cancel(false, p.err)
		} else {
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

函数流程如下:

  1. parent.Done() == nilparent 不会触发取消事件时,当前函数会直接返回
  2. child 的继承链包含可以取消的上下文时,会判断 parent 是否已经触发了取消信号:
    • 若已经被取消,child 会立刻被取消
    • 若没有被取消,child 会被加入 parentchildren 列表中,等待 parent 释放取消信号
  3. 当父上下文是开发者自定义的类型、实现了 context.Contextopen in new window 接口并在 Done() 方法中返回了非空的管道时:
    • 运行一个新的 Goroutine 同时监听 parent.Done()child.Done() 两个 Channel
    • parent.Done() 关闭时调用 child.cancel 取消子上下文

context.propagateCancelopen in new windowparentchild 之间同步取消和结束的信号,保证在 parent 被取消时,child 也会收到对应的信号。

context.cancelCtx.cancelopen in new window会关闭上下文中的 Channel 并向所有的子上下文同步取消信号:

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return
	}
	c.err = err
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done)
	}
	for child := range c.children {
		child.cancel(false, err)
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}

WithDeadline, WithTimeout

context.WithDeadlineopen in new windowcontext.WithTimeoutopen in new window 能创建可以被取消的计时器上下文 context.timerCtxopen in new window

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) // 已经过了截止日期
		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. 判断父上下文的截止日期与当前日期
  2. 使用 time.AfterFuncopen in new window 创建定时器
  3. 时间超过了截止日期后调用 context.timerCtx.cancelopen in new window 同步取消信号

context.timerCtxopen in new window

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

	deadline time.Time
}

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

func (c *timerCtx) cancel(removeFromParent bool, err error) {
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

context.timerCtx.cancelopen in new window会停止context和定时器。

6.1.4 传值

context.WithValueopen in new window能从父上下文中创建一个子上下文,传值的子上下文使用 context.valueCtxopen in new window 类型:

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

context.valueCtx.Valueopen in new window

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

context.valueCtxopen in new window 中存储的键值对与 context.valueCtx.Valueopen in new window 方法中传入的参数不匹配,就会从父上下文中查找该键对应的值直到某个父上下文中返回 nil 或者查找到对应的值。

Reference

  1. https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/open in new window
上次编辑于:
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v2.15.2