Go并发之Context篇

2023-10-30 15:37:38 浏览数 (2)

前言介绍:在学习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

0 人点赞