最近在学习Go语言,阅读Go语言并发之道(第四章),学习context包源码。

因为对context理解不深刻,所以写一篇文章总结一下;Go源码基于1.14.4版本。

 

1、为什么要用context,context能做什么

原因是:存在A调用B,B再调用C的场景,若此时A调用B取消了,那也要取消B调用C。

书上最初是通过Done Channel来引入“取消”机制的,然后再引入了context机制。

在Go1.7中,context包被引入标准库中,它成为考虑并发问题时的一个标准风格

 

2、理解Context包

参考链接:快速掌握 Golang context 包,简单示例

Context应该是一个链式的调用,通过WithCancel,WithDeadline,WithTimeout或WithValue派生出新的Context。

当父Context被取消时,其派生的所有子Context都将被取消。

 

调用context.WithXXX函数返回值有两个,一个是新的Context(后文以“子Context”代指),另一个是CancelFunc取消函数。

如果调用CancelFunc将取消子Context,移除父Context对子Context的引用,并停止所有子Context定时器。

举个例子:父Context->子Context1->子Context2

如果我们调用了 子Context1 的CancelFunc,那么子Context1 和 子Context2 都会被取消掉,同时父Context对子Context的引用也会被移除,就只剩下父Context了。如果没有调用CancelFunc,那么将泄漏子Context,直到父Context被取消或定时器触发为止。

go vet 工具检查所有流程,控制路径上使用 CancelFuncs。

 

P.S. CancelFunc 的定义如下,本质就是 func() .

 

Context接口:

Context接口关注以下四个函数:

Deadline()函数,Deadline() (deadline time.Time, ok bool) 返回截止时间time和ok值,当没有deadline的情况下,返回ok==false

Done() 函数,Done() <-chan struct{} 返回一个channel,当timeout或调用CancelFunc时channel会被close掉。

Err()函数,Err() error 返回一个错误,该Context为什么会被取消掉。

Value()函数,Value(key interface{}) interface{} 返回Context中与Key关联的Value,如果没有则返回nil

 

所有方法:

 

Background 和 TODO方法:

Background 和 TODO 方法返回emptyCtx(后文介绍)。

TODO的预期目的是作为一个占位符,当你不知道使用哪个Context的时候就可以用它。

一般我们将它们作为Context的根,来往下派生。

 

emptyCtx其实是一个int, 注意 type emptyCtx int 这一句代码。 其实现部分源码如下:

可以看到,emptyCtx本质是一个int,然后通过方法实现了Context接口的Deadline、Done、Err和Value四个接口函数。

根据介绍,它是一个永远不被取消的Context。

 

WithCancel方法:

WithCancel函数返回 cancelCtx(它包含父Context的拷贝 + new done channel…) 和 CancelFunc.

这里注意 newCancelCtx 的实现,同时结合下文的cancelCtx的实现来看。

 

简单理解调用propagateCancel函数的作用是将父Context和子Context的Cancel函数绑定起来。

这样父Context取消的时候,调用子Context的CancelFunc。

propagateCancel函数具体源码位置在: context.go Line 247行。

其内部实现用到了一个children map来存储这个关系(这个map在下文cancelCtx结构体内)。

 

cancelCtx解析:

《Go程序设计语言》Page81. Go允许我们定义不带名称的结构体成员,只需要指定类型即可;这种结构体成员成为匿名成员。

Context 就是 cancelCtx的匿名成员,这里不展示cancelCtx的具体四个方法的实现。

我们关注一下 children 这个map结构,它是我们上文提到的父子Context绑定的存储map.

 

WithCancel示例代码:

这里gen函数的意义是返回一个自动生成器,它会一直生产int整数到它返回的channel里面,直到它被取消。

示例函数结束时,defer 调用 cancel 方法,gen goroutine 将返回,不会泄漏。

同时注意,gen goroutine里面的dst也需要defer close.

 

 

WithDeadline方法:

WithDeadline函数返回timerCtx(父Context的一个拷贝 + deadline) 和 CancelFunc.

这里需要注意,如果父Context的截止时间已经早于d,其语义等效为父Context截止。

另外,timerCtx的期限时间满后、取消函数调用、父Context的DoneChannel关闭,任一发生都会取消子Context。

 

我们来看一下timerCtx的实现:

它拥有一个匿名的 cancelCtx, 另外需要注意的是,它的定时器是基于cancelCtx的Mutex来实现同步的。

 

WithDeadline示例代码:

其实很简单,timerCtx设置了Deadline为当前时间的50ms后,select语句等1秒或者timerCtx到期。

 

 

WithTimeout方法:

这里可以很明显的看出,WithTimeout的实现是基于 Deadline来实现的。

 

直接给出示例:

运行之后会发现,Timeout 和 Deadline的输出是一致的,都是 context deadline exceeded

 

 

WithValue方法:

可以看到,返回的是一个ValueCtx,它本质是一个Context + key-value。

 

WithValue示例:

具体的好处应该是:这样我们不仅保留了根节点原有的值,还在子孙节点中加入了新的值(继承)。

注意:若存在Key相同,则会被覆盖,这里具体看一下Value函数的实现方案:

它先判断自己的key是否符合,如果不符合就去找父Context的key,如果还不符合会继续往上找(P.S. 其他context都是这样类似的实现)

所以相同的Key会被覆盖,不同的key则会继承。

 

原理小结:

context包通过构建树形结构的Context,实现了上层goroutine对下层goroutine的控制。

 

使用原则:

  1. 不要把Context存在一个结构体当中。Context变量应该作为第一个参数显式地传入函数,一般命名为ctx。
  2. 即使方法允许,也不要传递一个空的Context,如果不确定需要什么Context,就传递一个context.TODO()。
  3. 使用context的Value相关方法只应该用于在程序和接口中传递请求相关的数据,不要用它传递一些可选参数。
  4. 同一个Context可以传递到不同的goroutine中,Context在多个goroutine中是安全的。
  5. 接收子Context的goroutine中,应该对子Context的Done Channel进行监控,一旦信道关闭就应当停止处理,释放资源并返回。

 

【Go】理解Go语言Context机制
Tagged on:
0 0 投票数
Article Rating
订阅评论
提醒

0 评论
内联反馈
查看所有评论