深度解密Go语言之context

2020-07-09 00:00:00 函数 节点 时间 调用 取消

Go 语言的 context 包短小精悍,非常适合新手学习。不论是它的源码还是实际使用,都值得投入时间去学习。

这篇文章依然想尝试全面、深入地去研究。文章相比往期而言,整体不长,希望你看完可以有所收获!

什么是 context

Go 1.7 标准库引入 context,中文译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。

context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。

随着 context 包的引入,标准库中很多接口因此加上了 context 参数,例如 database/sql 包。context 几乎成为了并发控制和超时控制的标准做法。

context.Context 类型的值可以协调多个 groutine 中的代码执行“取消”操作,并且可以存储键值对。重要的是它是并发安全的。
与它协作的 API 都可以由外部控制执行“取消”操作,例如:取消一个 HTTP 请求的执行。

没看懂?没关系,先往后看。

为什么有 context

Go 常用来写后台服务,通常只需要几行代码,就可以搭建一个 http server。

在 Go 的 server 里,通常每来一个请求都会启动若干个 goroutine 同时工作:有些去数据库拿数据,有些调用下游接口获取相关数据……



这些 goroutine 需要共享这个请求的基本数据,例如登陆的 token,处理请求的大超时时间(如果超过此值再返回数据,请求方因为超时接收不到)等等。当请求被取消或是处理时间太长,这有可能是使用者关闭了浏览器或是已经超过了请求方规定的超时时间,请求方直接放弃了这次请求结果。这时,所有正在为这个请求工作的 goroutine 需要快速退出,因为它们的“工作成果”不再被需要了。在相关联的 goroutine 都退出后,系统就可以回收相关的资源。

再多说一点,Go 语言中的 server 实际上是一个“协程模型”,也就是说一个协程处理一个请求。例如在业务的高峰期,某个下游服务的响应变慢,而当前系统的请求又没有超时控制,或者超时时间设置地过大,那么等待下游服务返回数据的协程就会越来越多。而我们知道,协程是要消耗系统资源的,后果就是协程数激增,内存占用飙涨,甚至导致服务不可用。更严重的会导致雪崩效应,整个服务对外表现为不可用,这肯定是 P0 级别的事故。这时,肯定有人要背锅了。

其实前面描述的 P0 级别事故,通过设置“允许下游长处理时间”就可以避免。例如,给下游设置的 timeout 是 50 ms,如果超过这个值还没有接收到返回数据,就直接向客户端返回一个默认值或者错误。例如,返回商品的一个默认库存数量。注意,这里设置的超时时间和创建一个 http client 设置的读写超时时间不一样,这里不详细展开。可以去看看参考资料【Go 在今日头条的实践】一文,有很精彩的论述。

context 包就是为了解决上面所说的这些问题而开发的:在 一组 goroutine 之间传递共享的值、取消信号、deadline……



用简练一些的话来说,在Go 里,我们不能直接杀死协程,协程的关闭一般会用 channel+select 方式来控制。但是在某些场景下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline 等,而且可以同时被关闭。再用 channel+select 就会比较麻烦,这时就可以通过 context 来实现。

一句话:context 用来解决 goroutine 之间退出通知元数据传递的功能。

context 底层实现原理

我们分析的 Go 版本依然是 1.9.2

整体概览

context 包的代码并不长,context.go 文件总共不到 500 行,其中还有很多大段的注释,代码可能也就 200 行左右的样子,是一个非常值得研究的代码库。

先给大家看一张整体的图:



上面这张表展示了 context 的所有函数、接口、结构体,可以纵览全局,可以在读完文章后,再回头细看。

整体类图如下:



接口

Context

现在可以直接看源码:

type Context interface {
    // 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
    Done() <-chan struct{}

    // 在 channel Done 关闭后,返回 context 取消原因
    Err() error

    // 返回 context 是否会被取消以及自动取消时间(即 deadline)
    Deadline() (deadline time.Time, ok bool)

    // 获取 key 对应的 value
    Value(key interface{}) interface{}
}

相关文章