GO的context
场景问题
通过done channel 可以用于协程间通信,由主协程告知子协程取消,结束,终止任务等
假如主协程中有多个任务1, 2, …m,主协程对这些任务有超时控制;而其中任务1又有多个子任务1, 2, …n,任务1对这些子任务也有自己的超时控制,那么这些子任务既要感知主协程的取消信号,也需要感知任务1的取消信号
如果还是使用done channel的用法,我们需要定义两个done channel,子任务们需要同时监听这两个done channel。如果层级更深,如果这些子任务还有子任务,那么使用done channel的方式将会变得非常繁琐且混乱
通过context,可以实现:
1.上层任务取消后,所有的下层任务都会被取消;
2.中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务以及同级任务
context实现
context是Go并发编程中常用到一种编程模式, 使用context可以使开发者方便的在这些goroutine里传递request相关的数据、取消goroutine的signal或截止日期
context接口
context包含4个接口方法
1 | type Context interface { |
Deadline:返回绑定当前context的任务被取消的截止时间;如果没有设定期限,将返回ok == false
Done:当绑定当前context的任务被取消时,将返回一个关闭的channel;如果当前context不会被取消,将返回nil
当一个父operation启动一个goroutine用于子operation,这些子operation不能够取消父operation。下面描述的WithCancel函数提供一种方式可以取消新创建的Context.
Context可以安全的被多个goroutine使用。开发者可以把一个Context传递给任意多个goroutine然后cancel这个context的时候就能够通知到所有的goroutine
Err:如果Done返回的channel没有关闭,将返回nil;如果Done返回的channel已经关闭,将返回非空的值表示任务结束的原因。如果是context被取消,Err将返回Canceled;如果是context超时,Err将返回DeadlineExceeded
Value: 返回context存储的键值对中当前key对应的值,如果没有对应的key,则返回nil
emptyCtx
1 | // An emptyCtx is never canceled, has no values, and has no deadline. It is not |
一般不会直接使用emptyCtx,emptyCtx没有任何功能,常用作顶层的context
通过Background和todo拿到两个emptyCtx实例:
Background:通常被用于主函数、初始化以及测试中,作为一个顶层的context,也就是说一般我们创建的context都是基于Background
TODO:是在不确定使用什么context的时候才会使用
valueCtx
1 | type valueCtx struct { |
0.有一个context变量作为父节点
1.用一个context变量,继承父节点的context的所有信息,并且携带kv结构,可以携带额外信息
2.valueCtx实现了Value方法,用以在context链路上获取key对应的值,如果当前context上不存在需要的key,会沿着context链向上寻找key对应的值,直到根节点, 获取value的过程就是在这条context链上由尾部上前搜寻
3.WithValue用以向context添加键值对
4.添加键值对不是在原context结构体上直接添加,而是以此context作为父节点,重新创建一个新的valueCtx子节点,将键值对添加在子节点上,由此形成一条context链
cancelCtx
1 | type cancelCtx struct { |
1.cancelCtx中也有一个context变量作为父节点
2.done表示一个channel,用来表示传递关闭信号
3.children是个map,存储子节点
4.err用于存储错误信息表示任务结束的原因
cancel方法
1 | func (c *cancelCtx) Done() <-chan struct{} { |
cancel 有两个参数,一个是 removeFromParent,表示当前的取消操作是否需要把自己从父 Context 中移除,第二个参数就是执行取消操作需要返回的错误提示
1.如果 c.done 是 nil (父 Context 是 emptyCtx 的情况),就赋值 closedchan, closedchan 是一个被关闭的channel
2.如果不是nil,就直接关闭。然后递归关闭子 Context。
3.关闭子 Context 的时候,removeFromParent 参数传值是 false,这是因为当前 Context 在关闭的时候,把 child 置成了 nil,所以子 Context 就不用再执行一次从父 Context 移除自身的操作了
WithCancel
WithCancel函数用来创建一个可取消的context,即cancelCtx类型的context。WithCancel返回一个context和一个CancelFunc,调用CancelFunc即可触发cancel操作
1 | type CancelFunc func() |
cancelCtx取消时,会将后代节点中所有的cancelCtx都取消
propagateCancel即用来建立当前节点与祖先节点这个取消关联逻辑
流程:
1.判断parent.Done() == nil,表明父节点以上的路径上没有可取消的context,不需要处理
2.如果在context链上找到到cancelCtx类型的祖先节点,则判断这个祖先节点是否已经取消,如果已经取消就取消当前节点;否则将当前节点加入到祖先节点的children列表
3.否则开启一个协程,监听parent.Done()和child.Done(),一旦parent.Done()返回的channel关闭,即context链中某个祖先节点context被取消,则将当前context也取消
timerCtx
timerCtx是一种基于cancelCtx的context类型,可以定时取消
1 | type timerCtx struct { |
内部使用 cancelCtx实现取消,定时器timer,和过期时间deadline实现定时取消功能
timerCtx取消时,也就是调用cancel方法,如上面注释所示
1.先将内部的cancelCtx取消,调用cancelCtx的cancel方法,
2.如果需要则将自己从cancelCtx祖先节点上移除
3.取消计时器
WithDeadline
通过该方法,可以返回一个基于parent的可取消的context,过期时间设置为d
1 | func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { |
1.如果父节点parent有过期时间并且过期时间早于d,那么新建的子节点context无需设置过期时间,使用WithCancel创建一个可取消的context
2.不满足以上条件,用parent和过期时间d创建一个定时取消的timerCtx,并建立新建context与可取消context祖先节点的取消关联关系
3.判断当前时间距离过期时间d的时长dur
4.dur小于0,即当前已经过了过期时间,则直接取消新建的timerCtx,原因为DeadlineExceeded
5.为新建的timerCtx设置定时器,一旦到达过期时间即取消当前timerCtx
WithTimeout
该方法最终也是withDeadline,只不过接收的不是一个时间点,而是一个时间时长timeout
1 | func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { |
使用场景
ctx控制goroutine生命周期
Context 可以被用来控制 goroutine 的生命周期,从而避免出现 goroutine 泄漏或者不必要的等待操作。
1 | func users(ctx context.Context, req *Request) { |
将 Context 对象作为参数传递给了 goroutine 函数,这样在请求被取消时,goroutine 就可以及时退出。
withValue传递数据
Context 还可以被用来在不同的 goroutine 之间传递请求域的相关数据。为了实现这个目的,我们可以使用Context 的 WithValue() 方法
1 | type key int |
在 goroutine 函数中,我们使用 ctx.Value()
方法来获取 Context 中保存的用户信息。
Context 中保存的键值对数据应该是线程安全的,因为它们可能会在多个 goroutine 中同时访问
withCancel取消操作
1 | func users(ctx context.Context, req *Request) { |
我们使用 WithCancel() 方法创建了一个可以取消的 Context 对象,并将取消操作封装在了一个 cancel() 函数中。然后我们启动了一个 goroutine 函数,使用 select 语句等待请求完成或者被取消,最后在主函数中等待一段时间后调用 cancel() 函数来取消请求。
withDeadline设置截止时间
1 | func users(ctx context.Context, req *Request) { |
我们使用 WithDeadline() 方法设置了一个截止时间为当前时间加上 1 秒钟的 Context 对象,并将超时操作封装在了一个 cancel() 函数中。然后我们启动了一个 goroutine 函数,使用 select 语句等待请求完成或者超时,最后在主函数中等待一段时间后调用 cancel() 函数来取消请求。
在使用 WithDeadline() 方法设置截止时间的时候,如果截止时间已经过期,则 Context 对象将被立即取消。
withTimeout设置超时时间
除了使用 WithDeadline() 方法进行截止时间设置之外,Context 还可以被用来设置超时时间
1 | func users(ctx context.Context, req *Request) { |
ctx传递
1 | func users(ctx context.Context, req *Request) { |
我们在处理 HTTP 请求的函数中创建了一个 Context 对象,并将它作为参数传递给了一个数据库查询函数 findUserByName()。在 findUserByName() 函数中,我们使用传递的 Context 对象来调用 db.QueryContext() 方法进行查询操作。由于传递的 Context 对象可能会在查询过程中被取消,因此我们需要在查询完成后检查查询操作的错误,以便进行相应的处理。
在进行 Context 的传递时,我们需要保证传递的 Context 对象是原始 Context 对象的子 Context,以便在需要取消操作时能够同时取消所有相关的 goroutine。如果传递的 Context 对象不是原始 Context 对象的子 Context,则取消操作只会影响到当前 goroutine,而无法取消其他相关的 goroutine。
使用总结
Context 可以用于管理 goroutine 的生命周期和取消操作,避免出现资源泄漏和死锁等问题,同时也可以提高应用程序的性能和可维护性。
- 在创建 goroutine 时,需要将原始 Context 对象作为参数传递给它。
- 在 goroutine 中,需要使用传递的 Context 对象来进行取消操作,以便能够及时释放相关的资源。
- Context 的传递时,需要保证传递的 Context 对象是原始 Context 对象的子 Context,以便在需要取消操作时能够同时取消所有相关的 goroutine。
- 在使用 WithCancel 和 WithTimeout 方法创建 Context 对象时,需要及时调用 cancel 函数,以便能够及时释放资源。
- 在一些场景下,可以使用 WithValue 方法将数据存储到 Context 中,以便在不同的 goroutine 之间共享数据。