什么是context
Package context defines the Context type, which carries deadlines,
cancellation signals, and other request-scoped values across API boundaries
and between processes.
在context的package中如此介绍context,很容易可以看出context的三个主要功能
- 携带截止时间
- 携带取消信号
- 在携带请求相关的值
作用范围为api边界和进程之间
为什么需要context
从context的介绍中就可以看出context的主要是为了进行协程取消或者并发控制,传值为一额外功能。
在Go语言的wiki中如此介绍 Go is syntactically similar to C, but with memory safety, garbage collection, structural typing,and CSP-style concurrency. 最后一项处理并发也就是Go语言的一大特性,这也就能理解为什么Go需要context来作为并发控制中重要的一环。
众所周知,Go语言有四个进行并发控制的工具
- 全局变量
- channel
- waitgroup
- context
要理解为什么context也是其中不可或缺的一环,不妨可以提出一个疑问: 如果没有context的话,会怎么样呢?
// withoutCancel 不取消协程 | |
func withoutCancel() { | |
go func() { | |
go func() { | |
defer fmt.Println("child dead") | |
for { | |
fmt.Println("child running...") | |
time.Sleep(1 * time.Second) | |
} | |
}() | |
for i := 0; i < 2; i++ { | |
fmt.Println("father running...") | |
time.Sleep(1 * time.Second) | |
} | |
defer fmt.Println("father dead") | |
}() | |
time.Sleep(5 * time.Second) | |
} | |
output: | |
father running... | |
child running... | |
child running... | |
father running... | |
father dead | |
child running... | |
child running... | |
child running... | |
child running... |
很明显这边father协程在结束后,child协程仍然在运行中,且最后并没有进行正常退出,那么是否有什么替代品来进行协程取消呢?
全局变量?明显不可,如果服务每次请求的控制都由同一个全局变量来控制,那很容易就会发生阻塞。
waitgroup是被设计用来控制多协程同步的,似乎也不适合用来控制单个协程。
那只能用channel来控制单个协程了,试下呗
// withChannel 使用通道取消协程 | |
func withChannel() { | |
go func() { | |
c := make(chan bool) | |
defer fmt.Println("father dead") | |
go func() { | |
defer fmt.Println("child dead") | |
for { | |
select { | |
case <-c: | |
return | |
default: | |
fmt.Println("child running...") | |
time.Sleep(1 * time.Second) | |
} | |
} | |
}() | |
for i := 0; i < 2; i++ { | |
time.Sleep(1 * time.Second) | |
fmt.Println("father running...") | |
} | |
c <- true | |
}() | |
time.Sleep(5 * time.Second) | |
} | |
output: | |
child running... | |
father running... | |
child running... | |
child running... | |
father running... | |
child dead | |
father dead |
这里看起来效果不错,father协程通过往channel中放入一个值来通知child协程结束。
如果有多个协程呢?往一个channel内塞多个值显然不可能,多个消费协程会进行争抢无法进行有效管理。如果是一个协程对应一个channel,新建多个channel的资源消耗不说,多个channel的管理与使用在协程数量变多后将会变得异常混乱复杂。而context可以很简单优雅的解决这一问题。
context源码及设计
Interface
先看下context的接口
type Context interface { | |
Deadline() (deadline time.Time, ok bool) // 返回context被取消的时间 | |
Done() <-chan struct{} // 返回一个channel,这个channel会在当前工作完成或者上下文被取消后关闭 | |
Err() error // 返回context结束的原因 | |
Value(key interface{}) interface{} | |
} |
在context包中有4个重要的类分别实现了这个interface
- emptyCtx
- valueCtx
- cancelCtx
- timerCtx
这四个类分别实现了context的强大功能
emptyCtx
type emptyCtx int
emptyCtx就是一个空的context,无法被取消,没有截止时间,没有值。
context.Background() 和 context.TODO()就是返回一个emptyCtx
valueCtx
type valueCtx struct { | |
Context | |
key, val interface{} | |
} |
valueCtx是一个用来携带key-value的context,可以看到一个valueCtx中只存了一个key和一个value,那么context是如何实现存储多个kv的呢,可以看下 func WithValue是怎么实现的
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} | |
} |
可以看出来每个valueCtx中还存有一个parentCtx,当context中需要存多个值时,实际上就是个valueCtx的linked list。context.Value()实际上就是通过遍历linked list来实现kv的查找的, 看下context.Value()的实现就可以很容易验证出来。
func (c *valueCtx) Value(key interface{}) interface{} { | |
if c.key == key { | |
return c.val | |
} | |
return c.Context.Value(key) | |
} |
cancelCtx
type cancelCtx struct { | |
Context | |
mu sync.Mutex // protects following fields | |
done chan struct{} // created lazily, closed by first cancel call | |
children map[canceler]struct{} // set to nil by the first cancel call | |
err error // set to non-nil by the first cancel call | |
} |
cancelCtx主要用来实现context 取消的功能。和valueCtx一样,一个cancelCtx中包含一个parentCtx,说明cancelCtx也拥有继承关系,另外其中还包含children map,看起来是一个树状的结构,等会可以观察下如何使用children。以及一个channel来实现context.Done()中返回的channel,一个mutex保证并发安全,以及一个err来记录取消的原因。
WithCancel()function可以生成一个cancelCtx,看下是如何实现的
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { | |
if parent == nil { | |
panic("cannot create context from nil parent") | |
} | |
c := newCancelCtx(parent) | |
propagateCancel(parent, &c) | |
return &c, func() { c.cancel(true, Canceled) } | |
} | |
func newCancelCtx(parent Context) cancelCtx { | |
return cancelCtx{Context: parent} | |
} |
可以看到和valueCtx一样,先把parentCtx存到cancelCtx中,再进行一个propagateCancel 传播取消的动作。看下propagateCancel是如何实现的
func propagateCancel(parent Context, child canceler) { | |
done := parent.Done() | |
if done == nil { | |
return // parent is never canceled | |
} | |
select { | |
case <-done: | |
// parent is already canceled | |
child.cancel(false, parent.Err()) | |
return | |
default: | |
} | |
p, ok := parentCancelCtx(parent) ok { | |
p.mu.Lock() | |
if p.err != nil { | |
// parent has already been canceled | |
child.cancel(false, p.err) | |
} else { | |
if p.children == nil { | |
p.children = make(map[canceler]struct{}) | |
} | |
p.children[child] = struct{}{} | |
} | |
p.mu.Unlock() | |
} else { | |
atomic.AddInt32(&goroutines, +1) | |
go func() { | |
select { | |
case <-parent.Done(): | |
child.cancel(false, parent.Err()) | |
case <-child.Done(): | |
} | |
}() | |
} | |
} |
前几步很简单,是对parent是否已经结束状态的一些检查。
p, ok := parentCancelCtx(parent)是从parent中一直往上寻找,看parent是不是也是继承自另外一个cancelCtx
如果parent确实是继承自一个cancelCtx,那么就把新的child也挂载到此cancelCtx的children map下。
如果没有,则新建一个协程监听parent是否取消,如果parent取消,child也会取消
可以看出来 cancelCtx其实是一个树状的结构,当parentCtx取消后,children map中的child ctx也都会进行取消的动作。
timerCtx
type timerCtx struct { | |
cancelCtx | |
timer *time.Timer // Under cancelCtx.mu. | |
deadline time.Time | |
} |
了解了cancelCtx之后timerCtx就非常简单,一个timerCtx内包含了一个cancelCtx来执行取消的操作,一个计时器以及一个时间来记录结束时间
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { | |
if parent == nil { | |
panic("cannot create context from nil parent") | |
} | |
if cur, ok := parent.Deadline(); ok && cur.Before(d) { | |
// The current deadline is already sooner than the new one. | |
return WithCancel(parent) | |
} | |
c := &timerCtx{ | |
cancelCtx: newCancelCtx(parent), | |
deadline: d, | |
} | |
propagateCancel(parent, c) | |
dur := time.Until(d) | |
if dur <= 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 { | |
c.timer = time.AfterFunc(dur, func() { | |
c.cancel(true, DeadlineExceeded) | |
}) | |
} | |
return c, func() { c.cancel(true, Canceled) } | |
} |
创建一个timerCtx也很容易,可以看出大部分和创建一个cancelCtx几乎一样,只要再新建一个timer计时器来进行cancel的操作即可。
思考: context真的是个好设计吗?
至此相信你应该以及知道context是如何实现的了。
context一直是被认为一个小而美的设计,context包确实也以一种巧妙的方式实现了context的这些功能。但是任然存在一些值得斟酌的点,以下观点并非全部来自个人,仅供讨论
Context everywhere! Context在go代码中像病毒一样蔓延,即使是不需要的代码也需要传递context。
相信你曾经问过或者被问过一个问题,这个函数里context要传什么呢?回答:传context.TODO()就行了,没啥原因,传就行了。
正是因为context在go代码中到处蔓延,所以才会出现context.TODO()这种让人匪夷所思的东西
valueCtx是否有必要?为什么用一个链表实现了map?
在context包一开始就有介绍写到 Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
相信大家在readability考试中看到过把整个结构体塞到context中来传递的行为,但是如何定义哪些数据可以放到context中呢,哪些又不可以呢?我们现在的各种日志插件,error插件,已经把这些信息也存到context中,这些也并非所谓的request-scoped data,这也是值得探讨的一个点。
Last but not least
ctx context.Context这个写法符合readability吗?