在上一篇文章中,讲了很多跟 Context
相关的东西,我们也知道了 go 里面 Context
的一些比较常见的用法、使用场景,比如超时控制、变量共享等, 但是对于 go
Context
本身还没有太多的讲解,可能看起来会有点费解,今天就来详细说说
Context
的设计以及其用法。
context.Context 模型
在开始之前,我们先来看看这张图,这张图涵盖了所有创建
context.Context
的方法:
首先,是最上层的 context.Background()
和
context.TODO()
,看过源码的同学应该知道,这两个方法返回的
Context
是一样的,都是 new(emptyCtx)
, 而这个
emptyCtx
其实是没有任何实际功能的,但是他们又是最重要的,因为创建
Context
只有这两个方法,其他的几个方法都是从这里创建的
Context
派生的。 我们一般会使用
context.Background()
来创建一个最顶级的
Context
,比如,go 的 http 服务器中
request.Context()
方法的那个 Context
就是通过
context.Background()
创建的。 而
context.TODO()
往往用在需要 Context
的地方,但是我们还没确定使用一个什么样的 Context
的时候。
其次,中间的 context.Context
表示通过
context.Background()
或者 context.TODO()
方法创建的 Context
。 这个 Context
往往就是一个请求中的根 Context
,所有子协程里面的
Context
都是从这个 Context
派生的,又或者是直接使用了这个 Context
。
然后,从父 Context
创建新的 Context
的几个方法需要详细说一下:
WithCancel
: 这个方法返回一个新的Context
,同时返回一个CancelFunc
,通过调用CancelFunc
,我们可以在子协程中的context.Done()
方法接收到取消的信号,从而作出相应的操作(比如清理、中止执行等)。WithDeadline
: 这个方法也会返回一个新的Context
,同时也返回了一个CancelFunc
,本质上来说,这两个返回值跟WithCancel
的两个返回值并无二致。我们通过WithDeadline
返回的CancelFunc
也是可以给子协程发送取消信号的。但是通过WithDeadline
创建的Context
,会有一个定时器在运行,到了指定时间如果我们的子协程依然没有结束,同样也会收到取消的信号,这个定时器的作用就是在指定时间后执行CancelFunc
。WithTimeout
: 这个其实跟WithDeadline
是一样的,只是参数上有点不一样,最终效果都是在一定时间后发送取消信号。WithValue
: 这个方法只是返回一个带有我们传递变量的新的Context
,没有其他什么特别的功能了。
所以,除了基础的 context.Background()
和
context.TODO()
,对于怎么基于这两个基础的
Context
创建新的
Context
,可以简单总结如下:
- 如果我们只是想有一个机制可以取消子协程的执行,可以使用
WithCancel
,拿到CancelFunc
之后,在我们需要的时候调用CancelFunc
就可以给子Context
传递取消信号。 - 如果我们想对子协程进行超时控制,可以使用
WithDeadline
或者WithTimeout
,这两个方法的本质上都是启动一个定时器,在到达一定时间后,会给子协程发送取消信号。但是除了定时器,它们还返回了一个CancelFunc
,这意味着我们在到达定时器指定的时间之前,也可以手动调用CancelFunc
来发送取消信号。 - 如果我们只是想给子协程传递一些数据,从而实现变量共享的话,可以使用
WithValue
。
实际使用中的 Context
我们再来看一张图,上面的描述可能会比较抽象,下面这个图展示了实际使用中的
Context
。
根结点的 Context
只有两种创建方式
context.Background()
或者
context.TODO()
,在我们做一些 io
操作的时候,比如 rpc 调用,数据库查询等,
我们会需要做一些超时控制,这个时候我们就会需要新建一个有超时控制功能的
Context
(使用 context.WithDeadline
或者
context.WithTimeout
), 假设是上图的
child 2
,然后 child 2
这个
Context
所在的 goroutine
里面也需要做一些
io
操作,然后也需要限制这些操作的超时时间, 然后在
child 2
的基础上再通过 context.WithTimeout
创建了一个新的 Context
,假设是 child 2-2
。
需要注意的是,这里每一级都是一个新的 Context 实例,而不是在原有 Context 上增加或者修改其属性。
假设 child 2
的 Deadline
到了,这个时候
child 2
的定时器会调用 CancelFunc
来给子
Context
发送取消信号。 child 2-2
里的
select
语句的 context.Done()
得以返回,从而开始执行清理操作,然后中止协程的执行。
在这个过程中,取消信号的传播是从上往下一级级有序传递的,每一级的
Context
会给那些从其派生的 Context
传传递取消信号,直到叶子结点。
需要注意的是,虽然信号传播是从上往下的,但是不代表子协程需要等待父协程的
context.Done()
里面的逻辑执行完再执行,因为我们之前也说过, 在 go 里面,协程是平等的,父子协程的执行是同时进行的。
我们可以看看下面的例子,有点啰嗦,大概看一下就好:
主要是想通过这个例子说明,在调用 CancelFunc
的时候,所有子孙 Context
都能接收到这个信号(当然它的父
Context
不会收到)。 这也跟我们实际的应用场景一致。
1 | package main |
整个过程大概如下图:
实际使用中的 goroutine
在实际的场景中,goroutine 类似
Context
,也是树状的结构,每一个协程都可以启动新的协程,同样子协程也可以启动新的协程,最终会如下图这样:
同样的,而在父协程里面通过 Context
发送取消信号的时候,所有子孙协程都能感知得到,所以虽然看起来这棵树可能变得有点庞大,但是也不是完全不可控的。
go 中监控协程的一个工具
我们现在直到了,go 的协程里面可以启动新的协程,最终可能会有非常多的协程,但是到底有多少呢?
对于这个问题,go 官方的标准库已经给我们提供了一个工具
net/http/pprof
,具体使用方式如下:
1 | package main |
通过 pprof
我们可以知道应用的健康状况,如协程数量等,这不是本文重点,不赘述了。
总结
本文主要讲述了如下内容:
- 我们先是讲解了创建
Context
的几种方式,其中,根Context
只有两种创建方式,分别是context.Background()
和context.TODO()
,其他种类的Context
可以通过context.WithXXX()
创建。 - 在 go 里面,如果我们只是想要取消一个协程,那么我们可以通过
WithCancel
来实现,如果要进行超时控制,可以使用WithTimeout
或WithDeadline
。 Context
是一个树状结构,每一个Context
都可以作为父Context
创建新的Context
,然后在调用CancelFunc
或者超时的时候,会由父到子传递取消的信号。Context
也可以用来传递参数,比如我们可以通过WithValue
来传递参数,然后在子协程里面通过Value
来获取参数。- 最后,我们讲解了如何通过
pprof
来监控协程的数量。