什么是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吗?