Golang Context 是好的设计吗?|转载

2020年2月11日 0 条评论 791 次阅读 0 人点赞

最近实现系统的分布式日志与事务管理时,在寻求所谓的全局唯一Goroutine ID无果之后,决定还是简单利用Context机制实现了基本的想法,不够高明,但是好用。于是对它当初的设计比较好奇,便有了此文。

1、What Context

Context是Golang官方定义的一个package,它定义了Context类型,里面包含了Deadline/Done/Err方法以及绑定到Context上的成员变量值Value,具体定义如下:

那么到底什么Context?

可以字面意思可以理解为上下文,比较熟悉的有进程/线程上线文,关于Golang中的上下文,一句话概括就是:goroutine的相关环境快照,其中包含函数调用以及涉及的相关的变量值。
通过Context可以区分不同的goroutine请求,因为在Golang Severs中,每个请求都是在单个goroutine中完成的。

2、Why Context

由于在Golang severs中,每个request都是在单个goroutine中完成,并且在单个goroutine(不妨称之为A)中也会有请求其他服务(启动另一个goroutine(称之为B)去完成)的场景,这就会涉及多个Goroutine之间的调用。如果某一时刻请求其他服务被取消或者超时,则作为深陷其中的当前goroutine B需要立即退出,然后系统才可回收B所占用的资源。
即一个request中通常包含多个goroutine,这些goroutine之间通常会有交互。

那么,如何有效管理这些goroutine成为一个问题(主要是退出通知和元数据传递问题),Google的解决方法是Context机制,相互调用的goroutine之间通过传递context变量保持关联,这样在不用暴露各goroutine内部实现细节的前提下,有效地控制各goroutine的运行。

如此一来,通过传递Context就可以追踪goroutine调用树,并在这些调用树之间传递通知和元数据。
虽然goroutine之间是平行的,没有继承关系,但是Context设计成是包含父子关系的,这样可以更好的描述goroutine调用之间的树型关系。

3、How to use

生成一个Context主要有两类方法:

3.1 )顶层Context:Background

要创建Context树,首先就是要创建根节点

该Context通常由接收request的第一个goroutine创建,它不能被取消、没有值、也没有过期时间,常作为处理request的顶层context存在。

3.2)下层Context:WithCancel/WithDeadline/WithTimeout

有了根节点之后,接下来就是创建子孙节点。为了可以很好的控制子孙节点,Context包提供的创建方法均是带有第二返回值(CancelFunc类型),它相当于一个Hook,在子goroutine执行过程中,可以通过触发Hook来达到控制子goroutine的目的(通常是取消,即让其停下来)。再配合Context提供的Done方法,子goroutine可以检查自身是否被父级节点Cancel:

:父节点Context可以主动通过调用cancel方法取消子节点Context,而子节点Context只能被动等待。同时父节点Context自身一旦被取消(如其上级节点Cancel),其下的所有子节点Context均会自动被取消。

有三种创建方法:

下面来看改编自Advanced Go Concurrency Patterns视频提供的一个简单例子:

输出结果:

注意,此时doSth方法中case之done的fmt.Println("done")并没有被打印出来。

超时场景:

输出结果:

4、Really elegant solution?

前面铺地了这么多。

确实,通过引入Context包,一个request范围内所有goroutine运行时的取消可以得到有效的控制。但是这种解决方式却不够优雅。

4.1 Like a virus

一旦代码中某处用到了Context,传递Context变量(通常作为函数的第一个参数)会像病毒一样蔓延在各处调用它的地方。比如在一个request中实现数据库事务或者分布式日志记录,创建的context,会作为参数传递到任何有数据库操作或日志记录需求的函数代码处。即每一个相关函数都必须增加一个context.Context类型的参数,且作为第一个参数,这对无关代码完全是侵入式的。

4.2 Context isn’t for cancellation

Context机制最核心的功能是在goroutine之间传递cancel信号,但是它的实现是不完全的。

Cancel可以细分为主动与被动两种,通过传递context参数,让调用goroutine可以主动cancel被调用goroutine。但是如何得知被调用goroutine什么时候执行完毕,这部分Context机制是没有实现的。而现实中的确又有一些这样的场景,比如一个组装数据的goroutine必须等待其他goroutine完成才可开始执行,这是context明显不够用了,必须借助sync.WaitGroup。

兰陵美酒郁金香

大道至简 Simplicity is the ultimate form of sophistication.

文章评论(0)

你必须 登录 才能发表评论