设为首页 加入收藏

TOP

深度解密Go语言之context(一)
2019-06-12 10:13:59 】 浏览:25
Tags:深度 解密 语言 context

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 同时工作:有些去数据库拿数据,有些调用下游接口获取相关数据……

request

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

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

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

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

request with context

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

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

context 底层实现原理

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

整体概览

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

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

structure

类型 名称 作用
Context 接口 定义了 Context 接口的四个方法
emptyCtx 结构体 实现了 Context 接口,它其实是个空的 context
CancelFunc 函数 取消函数
canceler 接口 context 取消接口,定义了两个方法
cancelCtx 结构体 可以被取消
timerCtx 结构体 超时会被取消
valueCtx 结构体 可以存储 k-v 对
Background 函数 返回一个空的 context,常作为根 context
TODO 函数 返回一个空的 context,常用于重构时期,没有合适的 context 可用
WithCancel 函数 基于父 context,生成一个可以取消的 context
newCancelCtx 函数 创建一个可取消的 context
propagateCancel 函数 向下传递 context 节点间的取消关系
parentCancelCtx 函数 找到第一个可取消的父节点
removeChild 函数 去掉父节点的孩子节点
init 函数 包初始化
WithDeadline 函数 创建一个有 deadline 的 context
WithTimeout 函数 创建一个有 timeout 的 context
WithValue 函数 创建一个存储 k-v 对的 context

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

整体类图如下:

classes

接口

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

Context 是一个接口,定义了 4 个方法,它们都是幂等的。也就是说连续多次调用同一个方法,得到的结果都是相同的。

Done() 返回一个 channel,可以表示 context 被取消的信号:当这个 channel 被关闭时,说明 context 被取消了。注意,这是一个只读的channel。 我们又知道,读一个关闭的 channel 会读出相应类型的零值。并且源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个 receive-only 的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不
编程开发网

首页 上一页 1 2 3 4 5 6 7 下一页 尾页 1/7/7
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇PHP转Go系列:数组与切片 下一篇Gin上传文件到MongoDB gridfs

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容:

array(4) { ["type"]=> int(8) ["message"]=> string(24) "Undefined variable: jobs" ["file"]=> string(32) "/mnt/wp/cppentry/do/bencandy.php" ["line"]=> int(217) }