为什么要有context?
在go的server中,通常每来一个请求都会启动若干个goroutine同时工作:有些去数据库拿数据,有些调用下游接口获取相关数据……
这些goroutine需要共享这个请求的基本数据,例如登陆的token,处理请求的最大超时时间等等。当请求被取消或者是处理时间太长,这时候,所有正在为这个请求工作的goroutine需要快速退出。
context包就是为了解决上面所说的这些问题而开发的:在一组goroutine之间传递共享的值、取消信号、deadline……一句话来说:context就是用来解决goroutine之间退出通知、元数据传递的功能。
context介绍
Go1.7加入了一个新的标准库context,它定义了Context类型,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用
对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以使用WithCancel、WithDeadline、WithTimeout或WithValue创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。
Context能够帮助我们处理并发中的协程关闭,常见的协程关闭情况:
1.goroutine自己跑完结束
2.主线程退出
3.通过信道发送关闭信号,引导协程关闭
context.Context 类型的值可以协调多个 groutine 中的代码执行“取消”操作,并且可以存储键值对。最重要的是它是并发安全的。
context的底层实现是mutex和channel的结合,前者用于初始化部分参数,后者用于通信
类型 | 作用 |
---|---|
Context | 接口,定义了Context接口的四个方法 |
EmptyCtx | 结构体,实现了Context接口,它其实是个空的context |
CancelFuc | 取消函数 |
Canceler | 接口,context取消接口,定义了两个方法 |
CancelCtx | 结构体,可以被取消 |
TimerCtx | 结构体,超时会被取消 |
ValueCtx | 结构体,可以存储k-v对 |
Background | 函数,返回一个空的context,常作为根context |
TODO | 函数,返回一个空的context,常用于重构时期,没有合适的context可用 |
WithCancel | 函数,基于父context,生成一个可以取消的context |
WithDeadline | 函数,创建一个有deadline的context |
WithTimeout | 函数,创建一个有timeout的context |
WithValue | 函数,创建一个存储k-v的context |
emptyCtx是一个空的context,本质上类型为一个整型;Done方法返回一个nil值,用户无论往nil中写入或者读取数据,均会陷入阻塞。Err方法返回的错误永远为nil;Value方法返回的value永远是nil。context.Background()和context.TODO()都是返回一个emptyCtx。
type emptyCtx int
cancelCtx
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of 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
}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent) //形成一个包裹着父context的context
propagateCancel(parent, &c)//传播cancel,父节点取消该节点也取消
return &c, func() { c.cancel(true, Canceled) }
}
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // 父亲没有被取消
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())//父亲被取消了,孩子也要被取消
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)//如果父亲是CancelCtx,并且父亲已经终止
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}//如果父亲也是CancelCtx,则只需要保证子context在父context的children当中去
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}//如果父亲不是CancelCtx,父亲拥有终止的能力
}
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)//根据这个cancelCtxKey来判断是否是cancelCtx
if !ok {
return nil, false
}
pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
return nil, false
}
return p, true
}
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)//如果channel未被创建
} else {
close(d)//channel被创建了,关闭只会就能从channel之中读到0值
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)//从父亲当中被移除
}
}
timerCtx
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err) //先把父类的cancel调用
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil { //把闹钟给停掉
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
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),//继承了父亲的CancelCtx
deadline: d,
}
propagateCancel(parent, c)//父亲的Cancelctx作用于当前context
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) }
}
valueCtx
当我们设置多个key-value对会使父context衍生出子context
valueCtx不适合作为存储介质,存放大量的kv数据,原因有三个:
1.一个valueCtx实例只能存一个kv对,因此n个kv对会嵌套n个valueCtx,造成空间浪费;
2.基于k寻找v的过程是线性的,时间复杂度为O(n)
3.不支持基于k的去重,相同k可能重复存在,并基于起点的不同,返回不同的v,因此可知,valueContext的定位类似于请求头,只适合存放少量作用域较大的全局meta数据
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}//如果key等于当前节点的key,就会返回val,否则进行进一步的匹配
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context//不断地取出它的父context去匹配
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case *emptyCtx:
return nil
default:
return c.Value(key)
}
}
}
func WithValue(parent Context, key, val any) 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}//生成一个包裹着父context的子context
}
context代码
//为啥需要context
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func f(ctx context.Context) {
defer wg.Done()
for {
fmt.Println("**")
time.Sleep(time.Millisecond * 500)
select {
case <-ctx.Done():
return
default:
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background()) //造一个取消的函数,Background()造一个根节点
wg.Add(1)
go f(ctx)
time.Sleep(time.Second * 5)
cancel() //调用cancel造的函数
wg.Wait()
//如何通知goroutine退出
}
package main
import (
"context"
"fmt"
"sync"
"time"
)
var wg sync.WaitGroup
func f(ctx context.Context) {
defer wg.Done()
for {
fmt.Println("**")
time.Sleep(time.Millisecond * 500)
select {
case <-ctx.Done():
return
default:
}
}
}
func main() {
d:=time.Now().Add(200*time.Millisecond)
ctx,cancel:=context.WithDeadline(context.Background(),d)
//func WithDeadline(parent Context,deadline time.Time)(Context,CancelFunc)
//到了daedline这个时间点,parent就会过期自动
//尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践
//如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间
defer cancel()
select{
case<-time.After(1*time.Second):
fmt.Println("周林")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
func WithTimeout(parent Context,timeout Duration)(Context,CancelFunc)
//取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的
例:
ctx,cancel:=context.WithTimeout(context.Background)(time.Millisecond*50)
//时间到了就自动取消
源码解读
type Context interface {
// 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
Done() <-chan struct{}
// 在 channel Done 关闭后,返回 context 取消原因(例如:超时或者取消)
Err() error
// 返回 context 是否会被取消以及自动取消时间(即 deadline)
Deadline() (deadline time.Time, ok bool)
// 获取 key 对应的 value
Value(key interface{}) interface{}
}
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
实现了上面定义的两个方法的 Context,就表明该 Context 是可取消的。源码中有两个类型实现了 canceler 接口:*cancelCtx 和 *timerCtx。注意是加了 * 号的,是这两个结构体的指针实现了 canceler 接口。
Context接口设计成这个样子的原因:
1.取消操作应该是建议性,而非强制性
caller不应该去关心、干涉callee的情况,决定如何以及何时return是callee的责任。caller只需发送“取消”信息,callee根据收到的信息来做进一步的决策,因此接口并没有定义cancel方法。
2.取消操作应该可传递
取消某个函数时,和它相关联的其他函数也应该取消。因此,Done()方法返回一个只读的channel,所有相关函数监听此channel。一旦channel关闭,通过channel的“广播机制”,所有监听者都能收到。
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 interface{}) interface{} {
return nil
}
background 通常用在 main 函数中,作为所有 context 的根节点。
todo 通常用在并不知道传递什么 context的情形。例如,调用一个需要传递 context 参数的函数,你手头并没有其他 context 可以传递,这时就可以传递 todo。这常常发生在重构进行中,给一些函数添加了一个 Context 参数,但不知道要传什么,就用 todo “占个位子”,最终要换成其他 context。
cancelCtx:
type cancelCtx struct {
Context
// 保护之后的字段
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
这是一个可以取消的 Context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context。
先来看 Done() 方法的实现:
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
c.done 是“懒汉式”创建,只有调用了 Done() 方法的时候才会被创建。再次说明,函数返回的是一个只读的 channel,而且没有地方向这个 channel 里面写数据。所以,直接调用读这个 channel,协程会被 block 住。一般通过搭配 select 来使用。一旦关闭,就会立即读出零值。
cancel方法的实现:
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 必须要传 err
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // 已经被其他协程取消
}
// 给 err 字段赋值
c.err = err
// 关闭 channel,通知其他协程
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
// 遍历它的所有子节点
for child := range c.children {
// 递归地取消所有子节点
child.cancel(false, err)
}
// 将子节点置空
c.children = nil
c.mu.Unlock()
if removeFromParent {
// 从父节点中移除自己
removeChild(c.Context, c)
}
}
总体来看,cancel() 方法的功能就是关闭 channel:c.done;递归地取消它的所有子节点;从父节点从删除自己。达到的效果是通过关闭 channel,将取消信号传递给了它的所有子节点。goroutine 接收到取消信号的方式就是 select 语句中的读 c.done 被选中。
timerCtx
timerCtx 基于 cancelCtx,只是多了一个 time.Timer 和一个 deadline。Timer 会在 deadline 到来时,自动取消 context。
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
timerCtx 首先是一个 cancelCtx,所以它能取消。看下 cancel() 方法:
func (c *timerCtx) cancel(removeFromParent bool, err error) {
// 直接调用 cancelCtx 的取消方法
c.cancelCtx.cancel(false, err)
if removeFromParent {
// 从父节点中删除子节点
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
// 关掉定时器,这样,在deadline 到来时,不会再次取消
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithTimeout 函数直接调用了 WithDeadline,传入的 deadline 是当前时间加上 timeout 的时间,也就是从现在开始再经过 timeout 时间就算超时。也就是说,WithDeadline 需要用的是绝对时间。重点来看它:
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
// 如果父节点 context 的 deadline 早于指定时间。直接构建一个可取消的 context。
// 原因是一旦父节点超时,自动调用 cancel 函数,子节点也会随之取消。
// 所以不用单独处理子节点的计时器时间到了之后,自动调用 cancel 函数
return WithCancel(parent)
}
// 构建 timerCtx
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: deadline,
}
// 挂靠到父节点上
propagateCancel(parent, c)
// 计算当前距离 deadline 的时间
d := time.Until(deadline)
if d <= 0 {
// 直接取消
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(true, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
// d 时间后,timer 会自动调用 cancel 函数。自动取消
c.timer = time.AfterFunc(d, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}