理解Go的Context机制
1. 什么是Context
最近在分析gRPC源码,proto文件生成部分的代码,接口函数的第一个参数统一是ctx context.Context
,对这种设计甚是迷惑,于是找些资料,对其背后的原理一探究竟。
Context
通常被译作上下文
,它是一个比较抽象的概念,一般理解为“程序单元的一个运行状态、现场、快照”。将content翻译为“上下文”,很好地诠释了其本质,说明了数据流的方向,上游会把内容传递给下游。在Go语言中,程序单元指的就是Goroutine。
每个Goroutine在执行之前,都要先知道“程序当前的执行状态”。通常,将这些执行状态封装在一个Context
变量中,传递到要执行的Goroutine中。上下文几乎已经成为传递“与请求Request具有相同生命周期的变量”的标准方法。在网络编程中,当接收网络请求Request时,以及处理网络请求Request时,我们可能需要开启不同的Goroutine,分别处理“获取请求数据”与“执行逻辑业务”。一个请求Request会在多个Goroutine中处理,而这些Goroutine可能需要共享Request的一些信息;同时,当该Request被取消或者超时的时候,依据这个Request创建的所有Goroutine也应该被结束。
2 context包
Context包的
介绍,请参考Go Concurrency Patterns: Context。
Go的设计者在设计Goroutine时,已经考虑到“多个Goroutine共享数据,以及多Goroutine管理机制”。golang.org/x/net/context包就是这种机制的实现。
context
包实现的主要功能为:
其一,在程序单元之间共享状态变量。
其二,在被调用程序单元的外部,通过设置ctx变量值,将“过期或撤销这些信号”传递给“被调用的程序单元”。
在网络编程中,若存在A调用B的API, B再调用C的API,若A调用B取消,那也要取消B调用C,通过在A、B、C的API调用之间传递Context
,以及判断其状态,就能解决此问题。这就是为什么gRPC的接口中都带上ctx context.Context
参数的原因之一。
Go1.7(当前是RC2版本)已将原来的golang.org/x/net/context
包挪入了标准库中,放在$GOROOT/src/context下面。标准库中net
、net/http
、os/exec
都用到了context
。同时为了考虑兼容,在原golang.org/x/net/context
包下存在两个文件,go17.go
是调用标准库的context
包,而pre_go17.go
则是之前的默认实现,其介绍请参考go程序包源码解读。
2.1 Context接口
context
包的核心就是Context
接口,其定义如下:
type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key interface{}) interface{}
}
Deadline方法
返回一个超时时间。到了该超时时间,该Context所代表的工作将被取消继续执行。Goroutine获得了超时时间后,可以对某些io操作设定超时时间。Done
方法 返回一个通道(channel)。当Context
被撤销或过期时,该通道被关闭。它是一个表示Context是否已关闭的信号。Err
方法 当Done通
道关闭后,Err
方法返回值为Contex
t被撤的原因。Value方法
可以让Goroutine共享一些数据,当然获得数据是协程安全的。但使用这些数据的时候要注意同步,比如返回了一个map,而这个map的读写则要加锁。
注意:context包里的方法是线程安全的,可以被多个线程使用。
Context
接口没有提供方法来设置其值和过期时间,也没有提供方法直接将其自身撤销。也就是说,Context
不能改变和撤销其自身。那么,该怎么通过Context
传递改变后的状态呢?
2.2 默认错误值
context包有两个默认错误值,一个表示Context被取消,另一个表示Context超期。
var Canceled = errors.New("context canceled")
var DeadlineExceeded = errors.New("context deadline exceeded")
2.3 emptyCtx类型
在context包中,emptyCtx结构体实现了context接口,是contex接口的实现类型之一。
type emptyCtx intfunc (*emptyCtx) Deadline() (deadline time.Time, ok bool) {return
}func (*emptyCtx) Done() <-chan struct{} {return nil
}func (*emptyCtx) Err() error {return nil
}func (*emptyCtx) Value(key interface{}) interface{} {return nil
}func (e *emptyCtx) String() string {switch e {case background:return "context.Background"case todo:return "context.TODO"}return "unknown empty Context"
}
我们不需要手动实现context接口类型的对象,context 包已经提供了两个context接口类型的对象:
var (background = new(emptyCtx)todo = new(emptyCtx)
)/*Background返回一个非nil、empty的上下文。这个上下文没有取消,没有值,并且没有期限。它通常用于由主功能,初始化和测试,并作为输入的顶层上下文。
*/
func Background() Context {return background
}/*TODO返回一个非nil、empty的上下文。在目前还不清楚要使用的上下文或尚不可用时,使用TODO函数。
*/
func TODO() Context {return todo
}
Background和TODO这两个函数都会返回一个context接口类型的实例,只是返回的这两个实例都是emptyCtx结构体类型。
2.4 cancelCtx结构体
cancelCtx结构体继承了Context,实现了canceler接口。
*cancelCtx 和 *timerCtx 都实现了canceler接口,实现该接口的类型都可以被直接canceled。
//*cancelCtx 和 *timerCtx 都实现了canceler接口,实现该接口的类型都可以被直接canceled。
type canceler interface {cancel(removeFromParent bool, err error)Done() <-chan struct{}
}type cancelCtx struct {Context // 匿名字段done chan struct{} // closed by the first cancel call.mu sync.Mutexchildren map[canceler]bool // set to nil by the first cancel callerr error // 当其被cancel时,将会把err设置为非nil。
}func (c *cancelCtx) Done() <-chan struct{} {return c.done
}func (c *cancelCtx) Err() error {c.mu.Lock()defer c.mu.Unlock()return c.err
}func (c *cancelCtx) String() string {return fmt.Sprintf("%v.WithCancel", c.Context)
}//核心是关闭c.done。
//同时会设置c.err = err, c.children = nil。
//依次遍历c.children,每个child分别cancel。
//如果设置了removeFromParent,则将c从其parent的children中删除。
func (c *cancelCtx) cancel(removeFromParent bool, err error) {if err == nil {panic("context: internal error: missing cancel error")}c.mu.Lock()if c.err != nil {c.mu.Unlock()return // already canceled}c.err = errclose(c.done)for child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err)}c.children = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c) // 从此处可以看到 cancelCtx的Context项是一个类似于parent的概念}
}
再来看一些Cancel相关的方法:
type CancelFunc func()// WithCancel方法返回一个继承自parent的Context对象,同时返回的cancel方法可以用来关闭返回的Context对象中的Done channel。
// 该方法将新建立的节点挂载在最近的、可以被cancel的父节点下(向下方向)。
// 如果传入的parent是不可被cancel的节点,则直接只保留向上关系。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := newCancelCtx(parent)propagateCancel(parent, &c)return &c, func() { c.cancel(true, Canceled) }
}func newCancelCtx(parent Context) cancelCtx {return cancelCtx{Context: parent,done: make(chan struct{}),}
}// 传递cancel
// 从当前传入的parent开始(包括该parent),向上查找最近的一个可以被cancel的parent。
// 如果找到的parent已经被cancel,则将刚才传入的child树给cancel掉。
// 否则,将child节点直接连接到找到的parent的children中(Context字段不变,即向上的父亲指针不变,但是向下的孩子指针变直接了)
//
// 如果没有找到最近的、可以被cancel的parent,即其上都不可被cancel,则启动一个goroutine等待传入的parent终止,则cancel传入的child树,或者等待传入的child终结。
func propagateCancel(parent Context, child canceler) {if parent.Done() == nil {return // parent is never canceled}if p, ok := parentCancelCtx(parent); ok {p.mu.Lock()if p.err != nil {// parent has already been canceledchild.cancel(false, p.err)} else {if p.children == nil {p.children = make(map[canceler]bool)}p.children[child] = true}p.mu.Unlock()} else {go func() {select {case <-parent.Done():child.cancel(false, parent.Err())case <-child.Done():}}()}
}// 从传入的parent对象开始,依次往上找到一个最近的可以被cancel的对象,即cancelCtx或者timerCtx
func parentCancelCtx(parent Context) (*cancelCtx, bool) {for {switch c := parent.(type) {case *cancelCtx:return c, truecase *timerCtx:return &c.cancelCtx, truecase *valueCtx:parent = c.Contextdefault:return nil, false}}
}// 从父对象的children map中删除这个child。
func removeChild(parent Context, child canceler) {// 从parent开始,往上找到最近的一个可以cancel的父对象。p, ok := parentCancelCtx(parent)if !ok {return}p.mu.Lock()if p.children != nil {delete(p.children, child)}p.mu.Unlock()
}
2.5 timerCtx结构体
timerCtx是一个继承自cancelCtx的结构体。
type timerCtx struct {cancelCtx // 此处的封装是为了继承来自于cancelCtx的方法,cancelCtx.Context才是父亲节点的指针timer *time.Timer // Under cancelCtx.mu. 是一个计时器deadline time.Time
}func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {return c.deadline, true
}func (c *timerCtx) String() string {return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, c.deadline.Sub(time.Now()))
}// 与cencelCtx有所不同,其除了处理cancelCtx.cancel,还回对c.timer进行Stop(),并将c.timer=nil
func (c *timerCtx) cancel(removeFromParent bool, err error) {c.cancelCtx.cancel(false, err)if removeFromParent {// Remove this timerCtx from its parent cancelCtx's children.removeChild(c.cancelCtx.Context, c)}c.mu.Lock()if c.timer != nil {c.timer.Stop()c.timer = nil}c.mu.Unlock()
}
由此结构体衍生出两个方法WithDeadline和WithTimeOut:
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {// 如果parent的deadline比新传入的deadline要早,则直接WithCancel。因为新传入的deadline没有效,父亲的deadline会先到期。if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {// The current deadline is already sooner than the new one.return WithCancel(parent)}c := &timerCtx{cancelCtx: newCancelCtx(parent),deadline: deadline,}// 接入树propagateCancel(parent, c)// 检查如果已经过期,则cancel新的子树d := deadline.Sub(time.Now())if d <= 0 {c.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(true, Canceled) }}c.mu.Lock()defer c.mu.Unlock()if c.err == nil {// 还没有被cancel的话,就设置deadline之后cancel的计时器c.timer = time.AfterFunc(d, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }
}// timeout和deadline本质一样
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout))
}
2.6 valueCtx结构体
valueCtx主要用来传递一些元数据,通过WithValue()来传入继承,通过Value()来读取。简单,不赘述。
func WithValue(parent Context, key interface{}, val interface{}) Context {return &valueCtx{parent, key, val}
}type valueCtx struct {Contextkey, val interface{}
}func (c *valueCtx) String() string {return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}func (c *valueCtx) Value(key interface{}) interface{} {if c.key == key {return c.val}return c.Context.Value(key)
}
3. context的使用
Goroutine的创建和调用关系是分层级的。更靠顶部的Goroutine应有办法主动关闭其下属的Goroutine的执行(否则,程序就可能失控)。为了实现这种关系,Context结构像一棵树,叶子节点须总是由根节点衍生出来的。
3.1 根节点
要创建Context树,第一步就是要得到根节点。可以使用context.Background()或context.TODO()来获取,一般是使用context.Background()。
context.Background()
返回一个emptyCtx类型的对象,该Context一般由接收请求的第一个Goroutine创建,是与进入请求对应的Context根节点。它不能被取消、没有值、也没有过期时间,常常作为处理Request的顶层context存在。通过WithCancel、WithTimeout函数来创建子对象,其可以获得cancel、timeout的能力。
context.TODO()也
返回一个emptyCtx类型的对象。在目前还不清楚要使用的上下文时,或上下文尚不可用时,使用context.TODO()生成的Context接口类型的对象。
3.2 子节点
有了根节点,该怎么创建它的子节点、孙节点呢?context包提供了多个函数来创建他们,如下所示:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key interface{}, val interface{}) Context
这些函数都接收一个Context
类型的参数parent
,并返回一个Context
类型的值。这样,就层层创建出不同的节点。子节点是从复制父节点得到的,并且根据接收参数设定子节点的一些状态值。接着,就可以将子节点传递给下层的Goroutine了。
再回到之前的问题:该怎么通过Context
传递改变后的状态呢?使用Context
的Goroutine无法取消这个操作,这是符合常理的。因为这些Goroutine是被某个父Goroutine创建的,而理应只有父Goroutine可以取消操作。在父Goroutine中,可以通过WithCancel方法获得一个cancel方法,从而获得cancel的权利。
3.2.1 WithCancel
函数
WithCancel
函数将父节点复制到子节点,并且返回一个额外的CancelFunc
函数类型变量,该函数类型的定义为:
type CancelFunc func()
调用CancelFunc
函数类型变量的对象,将撤销对应的Context
对象。这就是主动撤销Context
的方法。在父节点的Context
所对应的环境中,通过WithCancel
函数不仅可创建子节点的Context
,同时也获得了该节点Context
的控制权。一旦执行该函数,则该节点Context
就结束了。子节点需要类似如下代码来判断是否已结束,并退出该Goroutine:
select {case <-cxt.Done():// do some clean...
}
3.2.2 WithDeadline
函数
WithDeadline
函数的作用也差不多,它返回的Context类型值同样是parent
的副本,但其过期时间由deadline
和parent
的过期时间共同决定。当parent
的过期时间早于传入的deadline
时间时,返回的过期时间应与parent
相同。父节点过期时,其所有的子孙节点必须同时关闭;反之,返回的父节点的过期时间则为deadline
。
3.2.3 WithTimeout
函数
WithTimeout
函数与WithDeadline
类似,只不过它传入的是从现在开始Context剩余的生命时长。他们都同样也都返回了所创建的子Context的控制权,一个CancelFunc
类型的函数变量。
当顶层的Request请求函数结束后,我们就可以cancel掉某个context,从而层层Goroutine根据判断cxt.Done()
来结束。
3.2.4 WithValue
函数
WithValue
函数返回parent
的一个副本,调用该副本的Value(key)方法将得到val。这样我们不光将根节点原有的值保留了,还在子孙节点中加入了新的值。注意:若存在Key相同,则会被覆盖。
4. Context使用原则
- Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:
使用Context的程序包需要遵循如下的原则来满足接口的一致性以及便于静态分析。在子Context被传递到的goroutine中,应该对该子Context的Done通道(channel)进行监控。一旦该通道被关闭(即上层运行环境撤销了本goroutine的执行),应主动终止对当前请求信息的处理,释放资源并返回。
- Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx;
不要把Context存在一个结构体当中,要显式地传入函数。Context变量需要作为第一个参数使用,一般命名为ctx;
- Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use;
即使方法允许,也不要传入一个nil的Context。如果你不确定要用什么Context,那么传一个context.TODO;
- Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions;
使用context的Value方法时,只应该在程序和接口中传递“和请求相关的元数据”,不要用它来传递一些可选的参数;
- The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines;
同样的Context可以传递到不同的goroutine中,Context在多个goroutine中是安全的。
5. 总结
context
包通过构建树型关系的Context,来实现“上一层Goroutine能够对下一层Goroutine的控制”。对于处理一个Request请求操作,需要采用context
来层层控制Goroutine,以及传递一些变量来共享。
Context对象的生存周期一般仅为一个请求的处理周期。针对一个请求创建一个Context变量(它为Context树结构的根),在请求处理结束后,撤销此ctx变量,释放资源。
每次创建一个Goroutine,要么将原有的Context传递给Goroutine,要么创建一个子Context并传递给Goroutine。
Context能灵活地存储不同类型、不同数目的值,并且使多个Goroutine安全地读写其中的值。
当通过父Context对象创建子Context对象时,可同时获得子Context的一个撤销函数,这样父Context对象的创建环境就获得了对子Context将要被传递到的Goroutine的撤销权。
理解Go的Context机制相关推荐
- 深入理解js的执行机制
写在前面 javascript在浏览器中被浏览器的js引擎执行解释,从执行上下文的角度分析一下js的执行机制 执行上下文 执行上下文被定义成javascript引擎在处理理解js代码时,所创建的一个动 ...
- 深入理解Golang之context
深入理解Golang之context context是Go并发编程中常用到一种编程模式.本文将从为什么需要context,深入了解context的实现原理,以了解如何使用context. 作者:Tur ...
- 你有真正理解 Java 的类加载机制吗?| 原力计划
作者 | 宜春 责编 | Elle 出品 | CSDN 博客 你是否真的理解Java的类加载机制?点进文章的盆友不如先来做一道非常常见的面试题,如果你能做出来,可能你早已掌握并理解了Java的类加载机 ...
- OpenGL(5)深入理解Pipeline, State, Context
OpenGL(5)深入理解Pipeline, State, Context Pipeline(管线/管道) 管线(pipeline),可以理解为渲染流水线.它的最终目的是将输入3D数据经过几个流程的处 ...
- Android全面解析之Context机制
文章已授权『郭霖』公众号发布 前言 很高兴遇见你~ 欢迎阅读我的文章. 在文章Android全面解析之由浅及深Handler消息机制中讨论到,Handler可以: 避免我们自己去手动写 死循环和输入阻 ...
- 甘利俊一 | 信息几何法:理解深度神经网络学习机制的重要工具
智源导读:深度学习的统计神经动力学主要涉及用信息几何的方法对深度随机权值网络进行研究.深度学习技术近年来在计算机视觉.语音识别等任务取得了巨大成功,但是其背后的数学理论发展却很滞后.日本理化所的Shu ...
- (转载)彻底理解浏览器的缓存机制
彻底理解浏览器的缓存机制 2018/04/16 概述 浏览器的缓存机制也就是我们说的HTTP缓存机制,其机制是根据HTTP报文的缓存标识进行的,所以在分析浏览器缓存机制之前,我们先使用图文简单介绍一下 ...
- mysql 锁机制 mvcc_轻松理解MYSQL MVCC 实现机制
轻松理解MYSQL MVCC 实现机制 轻松理解MYSQL MVCC 实现机制 #### 1. MVCC简介 ##### 1.1 什么是MVCC MVCC是一种多版本并发控制机制. ##### 1.2 ...
- 深入BCB理解VCL的消息机制
深入BCB理解VCL的消息机制 引子:本文所谈及的技术内容都来自于Internet的公开信息.由笔者在闲暇之际整理 后,贴出来以飴网友,姑且妄称原创.每次在国外网站上找到精彩文章的时候,心中都 会暗自 ...
最新文章
- 将日期yyyy-MM-dd转为数字大写的形式
- 方案里最常用的集群拓扑图(包含:多机集群、负载均衡、双机)
- 改进的二值图像像素标记算法及程序实现(含代码)
- var_export()函数的使用举例(后续添加其他的php输出函数)
- ubuntu下安装拼音输入法ibus
- 计算浮点数相除的余(信息学奥赛一本通-T1029)
- 按Sybase的PowerDesigner工具设计的数据库模型 --- 解析生成能兼容多种数据库的相应的C#底层代码...
- 报文交换(串行)和分组交换(并行)
- EN 1650化学消毒剂和防腐剂检测
- Android Studio Entry name *.xml collided解决方案
- html form提交heard,德普前妻Amber Heard戛纳合辑
- 拆解 米家扫地机器人_1699元!小米米家扫地机器人拆解:真复杂
- 【Java】29.常用API之lang.Throwable(异常情况大总结)
- Elasticsearch入门教程(六):Elasticsearch查询(二)
- D3.js 生成词云图
- ★☆★新书已经到手《Java程序员,上班那点事儿》正式销售纪念帖★☆★
- ps多行文字如何左右对齐
- 如何重装java tm_彻底重装JDK的方法
- [Java基础]JAVA的SWITCH语句(String)
- Python自动登陆淘宝并爬取商品数据
热门文章
- redis向指定ip主机开放远程连接权限(防骚扰)
- React消息订阅与转发机制实现兄弟组件传值
- java 验证sql正确_java检查sql语法是否正确
- WEB期末大作业 痒痒鼠游戏系统
- python 同花顺thstrader_Python 踩坑之旅进程篇其三pgid是个什么鬼 (子进程\子孙进程无法kill 退出的解法)...
- (Android开发)WiFi扫描列表有多个相同SSID的热点过滤
- Android系统打不开,Android Studio 打不开问题解决
- html5自定义变量,javascript中怎么定义全局变量?
- CALayer 详解 -----转自李明杰
- Python逃生游戏