context原理
context是什么
context是go语言在1.7引入的一个用于goroutine
之间传递信息的并发安全的包,context可以翻译为上下文,其在项目中主要是用于上下与下层goroutine
的取消控制以及数据共享,也是go语言中goroutine
之间通信的一种方式,其底层是借助channl
与sync.Mutex
实现的。
关于context的用法我们在前一章节并发实践里已经做过介绍,本章主要介绍一下context的底层原理。
context的底层实现
与context相关的源码基本都在src/context/context.go
中,我们通过源码来看一下,context的底层究竟做了些什么。
context在底层实现上其实用到了2个接口,对这个接口的4种实现,以及提供了6个方法:
接口:
接口名 |
说明 |
Context |
context的接口定义,规定context的实现必须包含的四个基本方法 |
canceler |
context的取消接口,其中定义了两个方法 |
实现:
context接口的四种实现
结构名 |
说明 |
emptyCtx |
一个空的context,用作根context |
cancelCtx |
可以通过取消函数来取消context |
timerCtx |
可以通过定时器和deadline来定时取消context |
valueCtx |
类似于map,可以用来存储key/value键值对 |
方法:
函数名 |
说明 |
Background |
返回一个根context即emptyCtx |
TODO |
也是返回一个根context即emptyCtx |
WithCancel |
派生出一个cancelCtx |
WithDeadline |
派生出一个timerCtx |
WithTimeout |
派生出一个timerCtx |
WithValue |
派生出一个valueCtx |
下面我们将逐一解读这几个结构及其实现方法。
接口说明
context接口
首先还是回顾一下context接口,context的接口定义如下:
Go |
---|
| type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
|
接口提供了四个方法:
-
Deadline
: 返回 context.Context
被取消的时间,即截止时间;
-
Done
: 返回一个 channel,当Context被取消或者到达截止时间,这个 channel 就会被关闭,表示context结束,多次调用 Done
方法返回的channel是同一个;
-
Err
: 返回 context.Context
结束的原因;
-
Value
:从 context.Context
中获取键对应的值,类似于map的 get
方法,对于同一个context,多次调用 Value
并传入相同的 key
会返回相同的结果,如果没有对应的 key
,则返回 nil
,键值对是通过 WithValue
方法写入。
canceler接口
canceler接口的源码定义如下:
Go |
---|
| type canceler interface {
cancel(removeFromParent bool, err error) // 创建cancel接口实例的goroutine 调用cancel方法通知被创建的goroutine退出
Done() <-chan struct{} // 返回一个channel,后续被创建的goroutine通过监听这个channel的信号来完成退出
}
|
canceler接口主要用于取消方法的实现,如果一个示例既实现了context接口又实现了canceler接口,那么这个context就是可以本取消的,比如cancelCtx
和timerCtx
。如果仅仅只是实现了context接口,而没有实现canceler,就是不可取消的,比如emptyCtx
和valueCtx
。
context实现
在context包下对context接口有四种基本的实现,即emptyCtx
,cancelCtx
,timerCtx
,valueCtx
。
emptyCtx
首先看一下emptyCtx
这个最基本的实现,emptyCtx
虽然实现了context接口,但是不具备任何功能,因为实现很简单,基本都是直接返回空值。虽然emptyCtx
没有任何功能,但它还是有作用的,一般用它作为根context来派生出有实际用处的context。要想创建有实际功能的context,要使用后续提供的一系列with
方法来派生出新的context,这个在前面讲context用法的时候已经做过介绍,就不再过多赘述。
emptyCtx
的相关源码:
Go |
---|
| // An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*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 any) any {
return nil
}
|
可以看到emptyCtx
的实现没有做任何操作,就是一个整形结构。这个空的emptyCtx
会在两个创建根context的函数中被用到:
Go |
---|
| func Background() Context {
return background
}
func TODO() Context {
return todo
}
|
而这里Background
和TODO
其实就是返回一个emptyCtx
:
Go |
---|
| var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
|
在开发中,调用这两个函数Background()
或者TODO()
创建最顶层的context其实就是获取一个emptyCtx
。
cancelCtx
cancelCtx
结构定义如下:
Go |
---|
| type cancelCtx struct {
Context // 组合了一个Context ,所以cancelCtx 一定是context接口的一个实现
mu sync.Mutex // 互斥锁,用于保护以下三个字段
// value是一个chan struct{}类型,原子操作做锁优化
done atomic.Value
// key是一个取消接口的实现,map其实存储的是当前canceler接口的子节点,当前context被取消时,会遍历子节点发送取消信号
children map[canceler]struct{}
err error // context被取消的原因
}
|
下面看一下其各个方法的具体实现,首先看一下Done()
方法:
Go |
---|
| func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
|
代码很简单,其实就是采用“懒汉模式”创建一个struct{}
类型的channel返回,从类型可以看出这个channel是只读的,不能往里面写数据,所以应该避免直接读取这个channel,会发生阻塞。所以在使用上要配合select
来非阻塞读取,由于是只读的,所以只有在一种情况下会读到值,那就是关闭这个channel的时候会读到零值。利用这个而特性就可以实现关闭的消息通知。
再看一下其 cancel()
方法的实现:
Go |
---|
| // cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil { // context被取消的原因,必传,否则panic
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil { // 在赋值这个err之前,c.err已经有值了,说明已经被调用过cancel函数了,c这个context已经被取消
c.mu.Unlock()
return // already canceled
}
c.err = err // 赋值err信息
d, _ := c.done.Load().(chan struct{}) // 获取通知管道
if d == nil {
c.done.Store(closedchan)
} else {
close(d) // 关闭管道
}
// 遍历当前context的所有子节点,调用取消函数
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err) // 递归取消子context
}
c.children = nil // 取消动作完成之后,孩子节点置空
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c) // 将自身从父节点children map种移除
}
|
cancel不仅取消当前context,还会遍历当前context的所有子context,递归取消,递归取消玩当前context的所有子context后,会将自身从父节点children map
中移除,移除函数removeChild
源码如下:
Go |
---|
| // removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child) // 从父context的children中移除
}
p.mu.Unlock()
}
|
移除前后效果如下图所示:

在用户层面,创建cancelCtx
的方法其实我们你之前也接触过,就是withCancel
方法,在平常代码中,我们一般用这个方法来派生一个可以用cancel取消函数取消的context,常规用法如下:
Go |
---|
| ctx, cancel := context.WithCancel(context.Background())
|
下面继续跟一下这个WithCancel
函数的源码:
Go |
---|
| func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil { // 传入的父context不能为空,否则报panic
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent) // 这里就会创建一个cancelCtx
propagateCancel(parent, &c) // 这里主要是关联父context ctx和子congtxt c的逻辑
return &c, func() { c.cancel(true, Canceled) } // 具体的取消函数cancel的实现
}
|
前面说了调用cancelFunc
函数可以级联取消子context,那么为什么可以级联取消呢?propagateCancel
函数就是用来做这个工作的,他将父context和子context关联起来,具体的关联逻辑,我们通过源码来分析:
Go |
---|
| // propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
done := parent.Done() // 获取父context的通信管道 chan struct{}
if done == nil { // done为空,说明父context不会被取消
return // parent is never canceled
}
select {
case <-done: // 通信管道收到了消息,说明父context已经被取消,不用重复取消了
// parent is already canceled
child.cancel(false, parent.Err()) // 但是父context已经取消,这里子context也应该要取消,由于还没有关联上,所以主动调用cancel取消关联
return
default:
}
if p, ok := parentCancelCtx(parent); ok { // 从父context中提取出cancelCtx结构
p.mu.Lock()
if p.err != nil { // 加锁后双重检查,再次检查父context有没有被取消
// parent has already been canceled
child.cancel(false, p.err) // 父context被取消,主动取消子context
} else { // 父context没有被取消
if p.children == nil {
p.children = make(map[canceler]struct{}) // 创建父context的children map
}
p.children[child] = struct{}{} // 把当前子context加入到children map里面
}
p.mu.Unlock()
} else { // 从父context中没有提取出cancelCtx结构
atomic.AddInt32(&goroutines, +1)
go func() { // 新起一个goroutine监控父子context的通信管道有没有取消信号
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
|
看一下这个提取父context的cancelCtx结构的parentCancelCtx
方法:
Go |
---|
| func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
// 从父context的取消信息管道为空,说明父context不会被取消
// closedchan is a reusable closed channel.
// var closedchan = make(chan struct{})
// done == closedchan,表明ctx不是标准的 cancelCtx,可能是自定义的结构实现了 context.Context 接口
if done == closedchan || done == nil { //
return nil, false
}
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) // 通过context的value方法从父context中提取出cancelCtx
if !ok {
return nil, false
}
pdone, _ := p.done.Load().(chan struct{}) // 判断父context里的通信管道和cancelCtx里的管道是否一致
if pdone != done { // 不一致,表明parent不是标准的cancelCtx
return nil, false
}
return p, true // 返回cancelCtx
}
|
总结一下通过WithCancel
函数在派生可取消的子context的过程中,通过propagateCancel
函数关联父子context可能遇到的几种情形:
-
父context的通信管道done
为空或者已经被取消,就不用关联了,直接取消当前子context即可;
-
父context可以被取消,但是还未被取消,并且父context可以提取出标准的cancelCtx
结构,则创建父context的children map
,将当前子context加入到这个map
中;
-
父context可以被取消,但是还未被取消,父context不能提取出标准的cancelCtx
结构,新起一个goroutine
监控父子context的通信管道有没有取消信号。
timerCtx
timerCtx
在cancelCtx
的基础上,又提供了截止时间的功能,不仅拥有像cancelCtx
一样,可以通过调用取消函数cancelFun
来取消子context的方式,还可以设置一个截止时间deadline
,在deadline
到来时,自动取消context。
首先看一下timerCtx
的结构定义:
Go |
---|
| type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
|
看到它内置了cancelCtx
,所以cancelCtx
拥有的功方法,他可以调用cancelCtx
的方法,能够主动取消context,再看一下timerCtx
自身的cancel
方法实现:
Go |
---|
| func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err) // 直接调用cancelCtx的cancel方
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c) // 将当前子context从父context中删除
}
c.mu.Lock()
if c.timer != nil { // 要关闭掉定时器,因为手动取消过一次了,如果不关闭,在deadline到来时,不会再次取消,造成错误
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
|
同样在用户层面,我们一般通过WithTimeout
或者WithDeadline
来创建一个timerCtx
:
Go |
---|
| ctx, cancel := context.WithDeadline(context.Background(),time.Now().Add(4*time.Second)) // 截止时间当前时间4s后
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) // 超时时间为4s后
|
在WithTimeout
内部其实也是调用了WithDeadline
,所以只用分析WithDeadline
方法即可:
Go |
---|
| func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil { // 父context为空,直接报panic
panic("cannot create context from nil parent")
}
// 如果父context的deadline早于这里要设置的子context的截止时间
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// 直接取消父context即可,不需要再管子context的取消时间,直接构建一个可以取消的子context
// 因为父context的到期时间早于子context,当父context被取消的时候,这个子context肯定会被级联取消
return WithCancel(parent)
}
// 创建timerCtx对象
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c) // 关联父子context
dur := time.Until(d) // 获取距离设置的子context过期时间的时间差
if dur <= 0 { // 时间差小于0,表示已经过期了,直接取消
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
// 根据时间差,创建一个定时器,到deadline的时候定时触发取消
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
|
所以,父context未取消的情况下,在创建timerCtx
的时候有两种情况:
valueCtx
valueCtx的作用与上述三个context有点不同,它不是用于父子context之间的取消的,而是用于数据共享。
作用类似于一个map
,不过数据的存储和读取是在两个context,用于goroutine
之间的数据传递。
valueCtx
的结构定义如下:
Go |
---|
| type valueCtx struct {
Context
key, val interface{}
}
|
valueCtx
内置了Context,所以他也是一个context接口的实现,但是其没有实现canceler接口,所以他不能用作context的取消,valueCtx
实现了String()
方法和Value
方法,String()
比较简单,就不细看了。
下面看一下Value
方法:
Go |
---|
| func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
|
方法很简单,就是向上递归的查找key
所对应的value
,如果找到则直接返回value
,否则查找该context的父context,一直顺着 context向上,最终找到根节点(一般是emptyCtx
),直接返回一个nil
。查找过程如下图:

从定义可以出valueCtx
中存储着一对键值对,具体是怎么用的呢?同样我们一般使用withValue
方法派生出一个valueCtx
:
Go |
---|
| ctx := context.WithValue(context.Background(),"key1","value1")
|
withValue
函数源码如下:
Go |
---|
| func WithValue(parent Context, key, val interface{}) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
|
withValue
的方法实现很简单,就是创建一个valueCtx
,将key
和value
设置到valueCtx
返回。