Go——context标准库

context 标准库

Go中的goroutine之间没有父与子的关系,也就没有所谓子进程退出后的通知机制,多个goroutine都是平行地被调度,多个goroutine如何协作工作涉及通信、同步、通知和退出四个方面。

  • 通信:chan通道当然是goroutine之间通信的基础,注意这里的通信主要是指程序的数据通道。
  • 同步:不带缓冲的chan提供了一个天然的同步等待机制;当然sync.WaitGroup也为多个goroutine协同工作提供一种同步等待机制。
  • 通知:这个通知和上面通信的数据不一样,通知通常不是业务数据,而是管理、控制流数据。要处理这个也好办,在输入端绑定两个chan,一个用于业务流数据,另一个用于异常通知数据,然后通过slct收敛进行处理。这个方案可以解决简单的问题,但不是一个通用的解决方案。
  • 退出:goroutine之间没有父子关系,如何通知goroutine退出?可以通过增加一个单独的通道,借通道和select的广播机制(close channel to broadcast)实现退出。

Go语言在语法上处理某个goroutine退出通知机制很简单。但是遇到复杂的并发结构处理起来就显得力不从心。实际编程中goroutine会拉起新的goroutine,新的goroutine又会拉起另一个新的goroutine,最终形成一个树状的结构,由于goroutine里并没有父子的概念,这个树状的结构只是在程序员头脑中抽象出来的,程序的执行模型并没有维护这么一个树状结构。怎么通知这个树状上的所有goroutine退出?仅依靠语法层面的支持显然比较难处理。为此G01.7
提供了一个标准库context来解决这个问题。它提供两种功能:退出通知和元数据传递context库的设计目的就是跟踪goroutine调用,在其内部维护一个调用树,并在这些调用树中传递通知和元数据。

1、context的设计目的

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

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

2、基本数据结构

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

Context接口

Context是一个基本接口,所有的Context对象都要实现该接口,context的使用者在调用接口中都使用Context作为参数类型。

type Context interface {
	//如果Context实现了超时控制,则该方法返回ok true,deadlline为超时时间,否则ok为false
	Deadline() (deadline time.Time, ok bool)

	//后端被调的goroutine应该监听该方法返回的chan,以便及时释放资源
	Done() <-chan struct{}

	//Done返回的chan收到通知的时候,才可以访问Err()获取取消原因
	Err() error

	//可以访问上游goroutine传递给下游goroutine的值
	Value(key interface{}) interface{}
}
canceler接口

canceler接口是一个扩展接口,规定了取消通知的Context具体类型需要实现的接口。context包中的具体类型cancelCtx和timerCtx都实现了该接口。示例如下:

//一个context对象如果实现了canceler接口,则可以被取消
type canceler interface {
	//创建cancel接口实例的goroutine调用cancel方法通知后续创建的goroutine退出
	cancel(removeFromParent bool, err error)
	//Done方法返回chan需要后端goroutine来监听,并及时退出
	Done() <-chan struct{}
}
empty Context接口

emptyCtx实现了Context接口,但不具备任何功能,因为其所有的方法都是空实现。其存在的目的是作为Context对象树的根(root节点)。因为context包,的使用思路就是不停地调用context包提供的包装函数来创建具有特殊功能的Context实例,每一个Context实例的创建都以上一个Context对象为参数,最终形成一个树状的结构。示例如下:

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
}

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

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}
cancelCtx

cancelCtx是一个实现了Context接口的具体类型,同时实现了conceler接口。conceler具有退出通知方法。注意退出通知机制不但能通知自己,也能逐层通知其children节点。示例如下:

//cancelCtx可以被取消,cancelCtx取消时会同时取消所有实现canceler接口的孩子节点
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     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) Done() <-chan struct{} {
	c.mu.Lock()
	if c.done == nil {
		c.done = make(chan struct{})
	}
	d := c.done
	c.mu.Unlock()
	return d
}

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

func (c *cancelCtx) String() string {
	return fmt.Sprintf("%v.WithCancel", c.Context)
}

// 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
	if c.done == nil {
		c.done = closedchan
	} else {
		//显示通知自己
		close(c.done)
	}
	//循环调用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)
	}
}
timerCtx

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 fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, time.Until(c.deadline))
}

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

yalueCtx是一个实现了Context接口的具体类型,内部封装了Context接口类型,同时封装了一个k/v的存储变量。valueCtx可用来传递通知信息。示例如下:

type valueCtx struct {
	Context
	key, val interface{}
}

func (c *valueCtx) String() string {
	return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

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

3、API函数

下面这两个函数是构造Context取消树的根节点对象,根节点对象用作后续With包装函数的实参。

func Background()Context
func TODO()Context

With包装函数用来构建不同功能的Context具体对象。

  1. 创建一个带有退出通知的Context具体对象,内部创建一个cancelCtx的类型实例。
    例如:
func WithCancel(parent Context)(ctx Context,cancel CancelFunc)
  1. 创建一个带有超时通知的Context具体对象,内部创建一个timerCtx的类型实例。例如:
func WithDeadline(parent Context,deadline time.Time)(Context,CancelFunc)
  1. 创建一个带有超时通知的Context具体对象,内部创建一个timerCtⅸ的类型实例。例如:
func WithTimeout (parent Context,timeout time.Duration)
(Context, CancelFunc)
  1. 创建一个能够传递数据的Context具体对象,内部创建一个valueCtx的类型实例。例如:
func WithValue(parent Context,key,val interface(})Context

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

4、辅助函数

前面描述的With开头的构造函数是给外部程序使用的API接口函数。Context具体对象的链条关系是在Wth函数的内部维护的。现在分析一下With函数内部使用的通用函数。

func propagateCancel(parent Context,child canceler)

有如下几个功能:

  1. 判断parent的方法Done()返回值是否是nil,如果是,则说明parent不是一个可取消的Context对象,也就无所谓取消构造树,说明child就是取消构造树的根。
  2. 如果parent的方法Done():返回值不是nil,则向上回溯自己的祖先是否是cancelCtx类型实例,如果是,则将child的子节点注册维护到那棵关系树里面。
  3. 如果向上回溯自己的祖先都不是cancelCtx类型实例,则说明整个链条的取消树是不连续的。此时只需监听parent和自己的取消信号即可。
func propagateCancel(parent Context, child canceler) {
	if parent.Done() == nil {
		return // parent is never canceled
	}
	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{})
			}
			//维护parent和child的关系
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		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) {
	for {
		switch c := parent.(type) {
		case *cancelCtx:
			return c, true
		case *timerCtx:
			return &c.cancelCtx, true
		case *valueCtx:
			parent = c.Context
		default:
			return nil, false
		}
	}
}
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()
}

5、context的用法

package main

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

func main() {
	//使用context.Background()构建一个WithCancel类型的上下文
	ctxa, cancel := context.WithCancel(context.Background())

	//work模拟运行并监测前端的退出通知
	go work(ctxa, "work1")

	//使用WithDeadline包装前面的上下文对象ctxa
	tm := time.Now().Add(3 * time.Second)
	ctxb, _ := context.WithDeadline(ctxa, tm)

	go work(ctxb, "work2")

	//使用WithValue包装前面的上下文对象ctxb
	oc := otherContext{ctxb}
	ctxc := context.WithValue(oc, "key", "andes, pass from main ")

	go workWithValue(ctxc, "work3")

	//故意sleep10秒,让work2、work3超时退出
	time.Sleep(10 * time.Second)

	//显示调用work1的cancel方法通知其退出
	cancel()

	//等待work1打印退出信息
	time.Sleep(5 * time.Second)
	fmt.Println("main stop")
}

type otherContext struct {
	context.Context
}

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(1 * time.Second)
		}
	}
}

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

//结果
work1 is running 
work2 is running 
work3 is running value=andes, pass from main  
work1 is running 
work2 is running 
work3 is running value=andes, pass from main  
work1 is running 
work3 is running value=andes, pass from main  
work2 is running 
work1 is running 
work3 get msg to cancel
work2 get msg to cancel
work1 is running 
work1 is running 
work1 is running 
work1 is running 
work1 is running 
work1 is running 
work1 get msg to cancel
main stop

使用Context的一般流程如下:

  1. 创建一个Context根对象。例如:
func Background()Context
func TODO()Context
  1. 包装上一步创建的Context对象,使其具有特定的功能。
    这些包装函数是context package的核心,几乎所有的封装都是从包装函数开始的。原因很简单,使用context包的核心就是使用其退出通知广播功能。示例如下:
func WithCancel(parent Context)(ctx Context,cancel CancelFunc)

func WithTimeout (parent Context,timeout time.Duration)(Context,CancelFunc)

func WithDeadline(parent Context,deadline time.Time)(Context,CancelFunc)

func Withvalue(parent Context,key,val interface())Context
  1. 将上一步创建的对象作为实参传给后续启动的并发函数(通常作为函数的第一个参数),每个并发函数内部可以继续使用包装函数对传进来的Context对象进行包装,添加自己所需的功能。
  2. 顶端的goroutine在超时后调用cancel退出通知函数,通知后端的所有goroutine释放资源。
  3. 后端的goroutine通过select监听Context…DoneO返回的chan,及时响应前端goroutine的退出通知,一般停止本次处理,释放所占用的资源。

6、使用context传递数据的争议

该不该使用context传递数据

首先要清楚使用context包主要是解决goroutine的通知退出,传递数据是其一个额外功能。可以使用它传递一些元信息,总之使用context传递的信息不能影响正常的业务流程,程序不要期待在context中传递一些必需的参数等,没有这些参数,程序也应该能正常工作。

在context中传递数据的坏处
  1. 传递的都是interface类型的值,编译器不能进行严格的类型校验。
  2. 从interface{}到具体类型需要使用类型断言和接口查询,有一定的运行期开销和性能损失。
  3. 值在传递过程中有可能被后续的服务覆盖,且不易被发现。
  4. 传递信息不简明,较晦涩;不能通过代码或文档一眼看到传递的是什么,不利于后续维护。
context应该传递什么数据
  1. 日志信息。
  2. 调试信息。
  3. 不影响业务主逻辑的可选数据。

context包提供的核心的功能是多个goroutine之间的退出通知机制,传递数据只是一个辅助功能,应谨慎使用context传递数据。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值