context.Context

前言

context是go语言的一个并发包,一个标准库,用于goroutine之间的退出通知。

一、为什么要context

Go中的goroutine之间没有父与子的关系,也就没有所谓子进程退出后的通知机制,多个goroutine被并行的调度,多个goroutine如何协作?

  • 利用select收敛 + 在输入端在绑定一个非业务chan。

这样处理某个goroutine是一个简单的方案,但这不是一个通用的解决方案,当并发结构非常复杂时,该方案就显得力不从心了。

实际编程中 goroutine 会拉起新的 goroutine ,新的 goroutine 又会拉起另一个新的 goroutine ,最终形成一个树状的结构,由于 goroutine 里并没有父子的概念, 这个树状的结构只是在程序员头脑中抽象出来的 ,程序的执行模型并没有维护这么一个树状结构 。怎么通知这个树状上的所有 goroutine 退出?仅依靠语法层面的支持显然比较难处理。

  • 为此 Go 1.7 提供了一个标准库 context 来解决这个问题。它提供两种功能: 退出通知元数据传递。 context库 的设计目的就是跟踪 goroutine 调用 , 在其内部维护一个调用树, 井在这些调用树中传递通知和元数据。

二、context有什么用

context 库的设计目的就是跟踪 goroutine 调用树,并在这些 gouroutine 调用树中传递通知和元数据。两个目的 :

  1. 退出通知机制一一通知可以传递给整个 goroutine 调用树上的每一个 goroutine 。
  2. 传递数据一一数据可 以传递给整个 goroutine 调用树上的每一个 goroutine。

三、基本数据结构

3.1、context包的整体工作机制

第一个创建 Context 的 goroutine被称为 root 节点。 root 节点负责创建一个实现 Context 接口的具体对象, 并将该对象作为参数传递到其新拉起的 goroutine , 下游的 goroutine 可 以继续封装该对象,再传递到更下游的 goroutine 。Context 对象在传递的过程中最终形成一个树状的数据结构,这样通过位于 root 节点(树的根节点) 的 Context 对象就能遍历整个 Context 对象树 , 通知和消息就可以通过 root 节点传递出去 ,实现了上游 goroutine 对下游 goroutine 的消息传递。

3.2 基本接口和结构体

  1. Context 接口,Context 是一个基本接 口 , 所有的 Context 对象都要实现该接 口, context 的使用者在调用接口中都使用 Context 作为参数类型 。
type Context interface {
	//如果 Context 实现了起时控制,则该方法返回 ok true, deadline 为超时时间,否则 ok 为 false
	Deadline() (deadline time.Time, ok bool)
	// 后端被调的 goroutine 应该监听该方法返回的 chan ,以便及时释放资源
	Done() <-chan struct{}
	//Done 返回的 ch an 收到通知的时候,才可以访问 Err ()获知因为什么原因被取消
	Err() error
	// 可以 访问上游 goroutine 传递给下游 goroutine 的位
	Value(key interface{}) interface{}
}
  1. canceler 接口,canceler 接口是一个扩展接口,规定了取消通知的 Context 具体类型需要实现的接口 。
    context 包中的具体类型*cancelCtx 和*timerCtx 都实现了该接口。
// 一个 context 对象如采实现了 canceler 接口,则可以被取消
type canceler interface {
	// 创建 cancel 接 口实例的 g o routine 调 用 cancel 方法通知后续创建的 gorou tine 退出
	cancel(removeFromParent bool, err error)
	// Done 方法返回的 chan 需妥后端 goroutine 来监听 , 并及时退出
	Done() <-chan struct{}
}
  1. empty Context结构体,emptyCtx 实现了 Context 接口,但不具备任何功能,因为其所有的方法都是空实现。其存在的目的是作为 Context 对象树的根( root 节点)。因为 context 包的使用思路就是不停地调用context 包提供的包装函数来创建具有特殊功能的 Context 实例 ,每一个 Context 实例的创建都以上一个 Context 对象为参数, 最终形成一个树状的结构。
type emptyCtx int
// 实现Context所有方法,就实现了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 interface{}) interface{} {
	return nil
}

func (e *emptyCtx) String() string {
	switch e {
	case background:
		return "context.Background"
	case todo:
		return "context.TODO"
	}
	return "unknown empty Context"
}

package 定义了两个全局变量和两个封装函数,返回两个 emptyCtx 实例对象,实际使用时通过调用这两个封装函数来构造 Context 的 root 节点。

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

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
	return background
}

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
	return todo
}
  1. cancelCtx结构体,cancelCtx 是一个实现了 Context 接口的具体类型,同时实现了 conceler 接口。 conceler 具有退出通知方法。注意退出通知机制不但能通知自己,也能逐层通知其 children 节点。
// cancelCtx 可以被取消, cancelCtx 取消时会同时取消所有实现 canceler 接口的孩子节点
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
}

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

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

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

type stringer interface {
	String() string
}

func contextName(c Context) string {
	if s, ok := c.(stringer); ok {
		return s.String()
	}
	return reflectlite.TypeOf(c).String()
}

func (c *cancelCtx) String() string {
	return contextName(c.Context) + ".WithCancel"
}

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
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) // 显示通知自己
	}
	// 通知孩子
	// 循环调用 children 的 cancel 函数,由于 parent 已经取消,所以此时 child 调用cancel 传入的是 false
	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)
	}
}
  1. timeCtx,timerCtx 是一个实现了 Context 接口 的具体类型 ,内部封装了 cancelCtx 类型实例 ,同时有一个 deadline 变量,用来实现定时退出通知。
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) String() string {
	return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
		c.deadline.String() + " [" +
		time.Until(c.deadline).String() + "])"
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
	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()
}
  1. valueCtx,valueCtx 是一个实现了 Context 接口的具体类型,内部封装了 Context 接口类型,同时封装了一个 k/v 的存储变量。 valueCtx 可用来传递通知信息。
type valueCtx struct {
	Context
	key, val interface{}
}

// stringify tries a bit to stringify v, without using fmt, since we don't
// want context depending on the unicode tables. This is only used by
// *valueCtx.String().
func stringify(v interface{}) string {
	switch s := v.(type) {
	case stringer:
		return s.String()
	case string:
		return s
	}
	return "<not Stringer>"
}

func (c *valueCtx) String() string {
	return contextName(c.Context) + ".WithValue(type " +
		reflectlite.TypeOf(c.key).String() +
		", val " + stringify(c.val) + ")"
}

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

3.3 API函数

  1. 构造root节点对象,用于后序With包装函数的实参。
func Background() Context 
func TODO() Context 
  1. With包装函数
// 带退出通知的Context
func WithCancel (parent Context) (ctx Context , cancel CancelFunc)
// 带超时通知的Context
func WithDeadline (parent Context , deadline time.Time ) (Context , CancelFunc)
func WithTimeout (parent Context , timeout time.Duration) (Context , 
CancelFunc )
// 带传递数据的Context
func WithValue(parent Context , key , val interface{}) Context

这些函数都有一个共同的特点-- parent 参数,其实这就是实现 Context 通知树的必备条件。在 goroutine 的调用链中, Context 的实例被逐层地包装并传递,每层又可以对传进来的 Context实例再封装自己所需的功能 ,整个调用树需要一个数据结构来维护,这个维护逻辑在这些包装函数内部实现。

3.4 辅助函数

前面描述的 With 开头的构造函数是给外部程序使用的 API 接口函数。 Context 具体对象的链条关系是在 With 函数的内 部维护的。现在分析一下 With 函数内部使用的通用函数。
func propagateCancel(parent Context , child canceler )有如下几个功能:

  1. 判断 parent 的方法 Done() 返回值是否是 nil ,如果是,则说明 parent 不是一个可取消的 Context 对象,也就无所谓取消构造树,说明 child 就是取消构造树的根。
  2. 如果 parent 的方法 Done() 返回值不是 nil ,则向上回溯自己的祖先是否是 cancerCtx 类型实例,如果是,则将 child 的子节点注册维护到那棵关系树里面。
  3. 如果向上回溯自己的祖先都不是 cancelCtx 类型实例,则说明整个链条的取消树是不连续的。此时只需监听 parent 和自己的取消信号即可。
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

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

	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		atomic.AddInt32(&goroutines, +1)
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

func parentCancelCtx(parent Context )(*cancelCtx , bool ) : 判断 parent 中 是否封装*cancelCtx 的字段,或者接口里面存放的底层类型是否是*cancelCtx 类型。

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	done := parent.Done()
	if done == closedchan || done == nil {
		return nil, false
	}
	p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
	if !ok {
		return nil, false
	}
	pdone, _ := p.done.Load().(chan struct{})
	if pdone != done {
		return nil, false
	}
	return p, true
}

func removeChild(parent Context , child canceler ) : 如果 parent 封装*cancelCtx类型字段,或者接口里面存放的底层类型是*cancelCtx 类型 ,则将其构造树上的 child 节点删除。

func removeChild(parent Context, child canceler) {
	p, ok := parentCancelCtx(parent)
	if !ok {
		return
	}
	p.mu.Lock()
	if p.children != nil {
		delete(p.children, child)
	}
	p.mu.Unlock()
}

3.5 context用法

package main

import (
	"context"
	"fmt"
	"time"
)

// context 基本用法

// 自定义context类型
type otherContext struct {
	context.Context
}

// 主函数
func main() {
	// 利用withCancel函数构建可取消的context
	ctxA, cancel := context.WithCancel(context.Background())
	// work 模拟运行,并检测前端的推出通知
	go work(ctxA, "work1")
	// 利用withDeadline函数构建超时通知的context
	ctxB, _ := context.WithDeadline(ctxA, time.Now().Add(3*time.Second))
	// work 模拟运行
	go work(ctxB, "work2")
	// 利用withValue函数构建可传递数据的context
	oc := otherContext{ctxB}
	go work(oc, "work3")
	ctxC := context.WithValue(oc, "key", "andes pass from main")
	// workWithValue 模拟运行
	go workWithValue(ctxC, "work3")
	// 故意停顿10s,让work2 和 work3 超时退出
	time.Sleep(time.Second * 10)
	// 让work1 cancel
	cancel()
	// 停顿5秒,让work1打印信息
	time.Sleep(time.Second * 5)
	fmt.Println("main stop")
}

// 获取Context传递的数据,等待前端的退出通知。
func workWithValue(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("%s get msg to cancel\n", name)
			return
		default:
			val := ctx.Value("key").(string)
			fmt.Printf("%s is running and value = %s\n", name, val)
			time.Sleep(time.Second * 100)
		}
	}
}

// 检测通知 & do something
func work(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("%s get msg to cancel\n", name)
			return
		default:
			fmt.Printf("%s is running\n", name)
			time.Sleep(4 * time.Second)
		}
	}
}

在这里插入图片描述

3.6 使用 context 传递数据的争议

  1. 该不该使用 context 传递数据
    首先要清楚使用 context 包主要是解决 goroutine 的通知退出,传递数据是其一个额外功能。可以使用它传递一些元信息 ,总之使用 context 传递的信息不能影响正常的业务流程,程序不要期待在 context 中传递一些必需的参数等,没有这些参数,程序也应该能正常工作。
  2. 在 context 中传递数据的坏处
  • 传递的都是 interface{} 类型的值,编译器不能进行严格的类型校验。
  • 从 interface{} 到具体类型需要使用类型断言和接口查闹,有一定的运行期开销和性能损失。
  • 值在传递过程中有可能被后续的服务覆盖,且不易被发现。
  • 传递信息不简明,较晦涩;不能通过代码或文档一眼看到传递的是什么,不利于后续维护。
  1. context 应该传递什么数据
  • 日志信息。
  • 调试信息
  • 不影响业务主逻辑的可选数据。
    context 包提供的核心的功能是多个 goroutine 之间的退出通知机制,传递数据只是一个辅助功能,应谨慎使用 context 传递数据。

总结

1)context 库的设计目的就是跟踪 goroutine 调用树,并在这些 gouroutine 调用树中传递通知和元数据。

参考资料

[1] Go 语言核心编程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值