前言介绍:在学习Go并发的时候,我们总是能够看到context,而这个context却只在go语言中存在。笔者在看到context的时候,便问了自己几个问题。
context是什么?是用来干什么的?我们为什么需要context? context是如何使用的?它为什么被设计成这个样子?
基于上面的这些问题,笔者做了整理,笔者觉得在知道了这些问题的答案之后,context 也算是有一点了解了。
1. 为什么Go需要context,它是用来干什么的?
原因:在golang中的创建一个新的协程并不会返回像c语言创建一个线程一样类似的pid,这样就导致我们不能从外部杀死某个线程,所以我们就得让它自己结束。(备注:goroutine不能返回pid的原因,应该是协程的实现原理有很大关系,多个协程对应1个线程的实现机制。)
当然我们可以采用channel+select的方式,来解决这个问题,不过场景很复杂的时候,我们就需要花费很大的精力去维护channel与这些协程之间的关系,这就导致了我们的并发代码变得很难维护和管理。例如:由一个请求衍生出多个协程,并且之间需要满足一定的约束关系,以实现一些诸如:有效期,中止线程树,传递请求全局变量之类的功能。
Context机制:context的产生,正是因为协程的管理问题,golang官方从1.7之后引入了context,用来专门管理协程之间的关系。
Google的解决方法是Context机制,相互调用的goroutine之间通过传递context变量保持关联,这样在不用暴露各goroutine内部实现细节的前提下,有效地控制各goroutine的运行。通过传递context就可以追踪goroutine 调用树,并在这些调用树之间传递通知和元数据。
虽然goroutine之间是平行的,没有继承关系,但是Context设计成是包含父子关系的,这样可以更好的描述goroutine调用之间的树型关系。
2. context的定义是什么样子的?
context是上下文的意思,一般理解为程序单元的一个运行状态、现场、快照,其中包含函数调用以及涉及的相关的变量值。
每个Goroutine在执行之前,都要先知道程序当前的执行状态,通常将这些执行状态封装在一个Context变量中,传递给要执行的Goroutine中。上下文则几乎已经成为传递与请求同生存周期变量的标准方法。
下面是https://golang.org/pkg/context/中提供的接口和常用API:
2.1 接口简介
1. context包里的方法是线程安全的,可以被多个线程使用。 2.当Context被canceled或是timeout, Done返回一个被closed 的channel。 3.在Done的channel被closed后, Err代表被关闭的原因如果存在。 4.Deadline 返回Context将要关闭的时间。 5.如果存在,Value 返回与 key 相关了的值,不存在返回 nil。
2.2 常用API
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // 一旦调用cancel,就会取消创建的ctx
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) // 带有效期的cancel, 到期之后会主动调用cancel
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) // 带超时时间的cancel,与WithDeadline类似,区别在于下面的参数是时间间隔
3.我们如何正确使用context?
step1:首先,需要创建context的根节点。
// 返回一个空的Context,它作为所有由此继承Context的根节点
func Background() Context (备注:这里创建的ctx,不能被取消、没有值、也没有过期时间,通常是在主协程或者第一个处理Request的协程中)
step2: 创建下层context节点。
通过2.2中的API来创建下层context节点,而这里创建好的下层context节点具有以下特点:
1.父节点Context可以主动通过调用cancel方法取消子节点Context。
2.子节点Context只能被动等待。
3.父节点Context自身一旦被取消(如其上级节点Cancel),其下的所有子节点Context均会自动被取消。
例子1: 主协程主动调用cancel() 取消子context
Output:
通过输出我们可以看出来,在主协程调用了cancel()之后,子协程中的ctx会被主动关闭掉,延迟时间是1秒,会看到打印done。
例子2: 超时之后,调用cancle()的例子
通过输出可以看出来,在2s超时之后,也就是done会主动打印出来,表明cancel()被主动调用了。(备注:warning提示可以被忽略掉,因为cancle()不被创建根ctx的协程主动调用就会提示这个告警。)
4.context是如何实现的呢?
(图片来自:https://zhuanlan.zhihu.com/p/34417106)
1. context的存储与查询:
context上下文数据的存储就像一个树,每个结点只存储一个key/value对。WithValue()保存一个key/value对,它将父context嵌入到新的子context,并在节点中保存了key/value数据。Value()查询key对应的value数据,会从当前context中查询,如果查不到,会递归查询父context中的数据。
备注:context中的上下文数据不是全局的,它只查询本节点及父节点们的数据,不能查询兄弟节点的数据。
2. cancel的实现:
(图片来自:https://zhuanlan.zhihu.com/p/34417106)
cancelCtx结构体中children保存它的所有子canceler, 当外部触发cancel时,会调用children中的所有cancel()来终止所有的cancelCtx。done用来标识是否已被cancel。当外部触发cancel、或者父Context的channel关闭时,此done也会关闭。
对于超时调用cancel(), 是因为timerCtx 中存储了一个超时时间,等到超时间到期之后,会主动调用cancel()。
调用cancel()之后的效果如下所示:
(图片来自:https://zhuanlan.zhihu.com/p/34417106)
参考资料: 理解 Go Context 机制:https://juejin.im/entry/58088180c4c971005879b184
Go进阶01:golang context 用法详解:https://mojotv.cn/2018/12/26/what-is-context-in-go
Golang之Context的使用:http://www.nljb.net/default/Golang之Context的使用/
golang中Context的使用场景:https://www.cnblogs.com/yjf512/p/10399190.html
Go Context的踩坑经历:https://zhuanlan.zhihu.com/p/34417106