go语言中的context|青训营

127 阅读7分钟

context机制

context:Context就是用来在父子goroutine间进行值传递以及发送cancel信号的一种机制。

context包实现的主要功能为: 其一,在程序单元之间共享状态变量。 其二,在被调用程序单元的外部,通过设置ctx变量值,将“过期或撤销这些信号”传递给“被调用的程序单元”。在网络编程中,若存在A调用B的API, B再调用C的API,若A调用B取消,那也要取消B调用C,通过在A、B、C的API调用之间传递Context,以及判断其状态,就能解决此问题。


type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}
type canceler interface
 {
    cancel(removeFromParent bool, err error)   
    Done() <-chan struct{}
}

1.Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期; 2.Done —返回一个 channel,可以表示 context 被取消的信号:当这个 channel 被关闭时,说明 context 被取消了。注意,这是一个只读的channel。我们又知道,读一个关闭的 channel 会读出相应类型的零值。并且源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个 receive-only 的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(零值)后,就可以做一些收尾工作,尽快退出。 3.Err — 返回 context.Context 结束的原因,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值; 如果 context.Context 被取消,会返回 Canceled 错误; 如果 context.Context 超时,会返回 DeadlineExceeded 错误; 4.Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;

实现了canceler定义的两个方法的 Context,就表明该 Context 是可取消的。源码中有两个类型实现了 canceler 接口:*cancelCtx 和 *timerCtx。注意是加了 * 号的,是这两个结构体的指针实现了 canceler 接口。

context 设计原理

在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。Go 服务的每一个请求都是通过单独的 Goroutine 处理的2,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。

如下图所示,我们可能会创建多个 Goroutine 来处理一次请求,而 context.Context 的作用是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期。 IMG_4.png 每一个 context.Context 都会从最顶层的 Goroutine 一层一层传递到最下层context.Context 可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。当最上层的 Goroutine 因为某些原因执行失败时,下层的 Goroutine 由于没有接收到这个信号所以会继续工作;但是当我们正确地使用 context.Context 时,就可以在下层及时停掉无用的工作以减少额外资源的消耗。 我们可以根据一段代码来理解context是如何进行信号同步的。

IMG_5.png func main() { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)//创建一个存活时间为1 秒的上下文 defer cancel()

	go handle(ctx, 500*time.Millisecond)//处理时间为0.5秒
	select {
	case <-ctx.Done()://等待ctx。done()1秒
		fmt.Println("main", ctx.Err())
	}
}

func handle(ctx context.Context, duration time.Duration) {
	select {
	case <-ctx.Done():
		fmt.Println("handle", ctx.Err()) //ctx运行结束
	case <-time.After(duration):
		fmt.Println("process request with", duration) //超时
	}
}

当该程序运行时,过期时间大于处理时间,所以我们有足够的时间处理该请求,所以主程序会在handle运行完之后,再等待0.5秒,执行fmt.Println("main", ctx.Err()) 故输出结果为

context常用方法

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)
 
/*
    Background返回一个非nil、empty的上下文。
    这个上下文没有取消,没有值,并且没有期限。
    它通常用于由主功能,初始化和测试,并作为输入的顶层上下文。
*/
func Background() Context {
    return background
}
 
 /*
    TODO返回一个非nil、empty的上下文。
    在目前还不清楚要使用的上下文或尚不可用时,使用TODO函数。
*/
func TODO() Context {
    return todo
}

context 包中最常用的方法还是 context.Background、context.TODO,这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用。

Goroutine的创建和调用关系是分层级的。更靠顶部的Goroutine应有办法主动关闭其下属的Goroutine的执行(否则,程序就可能失控)。为了实现这种关系,Context结构像一棵树,叶子节点须总是由根节点衍生出来的。 要创建Context树,第一步就是要得到根节点。可以使用context.Background()。context.TODO()来获取,一般是使用context.Background()。context.Background()返回一个emptyCtx类型的对象,该Context一般由接收请求的第一个Goroutine创建,是与进入请求对应的Context根节点。它不能被取消、没有值、也没有过期时间,常常作为处理Request的顶层context存在。通过WithCancel、WithTimeout函数来创建子对象,其可以获得cancel、timeout的能力。context.TODO()也返回一个emptyCtx类型的对象。在目前还不清楚要使用的上下文时,或上下文尚不可用时,使用context.TODO()生成的Context接口类型的对象。

创建子节点

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key interface{}, val interface{}) Context

这些函数都接收一个Context类型的参数parent,并返回一个Context类型的值。这样,就层层创建出不同的节点。子节点是从复制父节点得到的,并且根据接收参数设定子节点的一些状态值。接着,就可以将子节点传递给下层的Goroutine了。 该怎么通过Context传递改变后的状态呢?使用Context的Goroutine无法取消这个操作,这是符合常理的。因为这些Goroutine是被某个父Goroutine创建的,而理应只有父Goroutine可以取消操作。在父Goroutine中,可以通过WithCancel方法获得一个cancel方法,从而获得cancel的权利。

context子树的取消

我们可以从WithCancel开始看起。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

c := newCancelCtx(parent) 复制整个父上下文,并且私有化

propagateCancel(parent, &c) 会构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消。

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() == nil,当前函数会直接返回 2.当 child 的继承链包含可以取消的上下文时,会判断 parent 是否已经触发了取消信号; 如果父上下文被取消,子上下文会立即取消并返回 如果没有被取消,child 会被加入 parent 的 children 列表中,等待 parent 释放取消信号; parentCancelCtx(parent) :找到第一个可取消的父节点 3.当父上下文是开发者自定义的类型、实现了 context.Context 接口并在 Done() 方法中返回了非空的管道时; 运行一个新的 Goroutine 同时监听 parent.Done() 和 child.Done() 两个 Channel; 在 parent.Done() 关闭时调用 child.cancel 取消子上下文

context传递值

context 包中的 context.WithValue 能从父上下文中创建一个子上下文,传值的子上下文使用 context.valueCtx 类型,context.valueCtx 结构体会将除了 Value 之外的 Err、Deadline 等方法代理到父上下文中,它只会响应 context.valueCtx.Value 方法。