golang的context的理解
golang的context的理解
文章美化版
golang的context的理解
context介绍
在Go服务器中,每个传入请求都在其自己的goroutine中进行处理。 请求处理程序通常会启动其他goroutine来访问后端,例如数据库和RPC服务。 处理请求的goroutine集合通常需要访问特定于请求的值,例如最终用户的身份,授权令牌和请求的期限。 当一个请求被取消或超时时,处理该请求的所有goroutine应该迅速退出,以便系统可以回收他们正在使用的任何资源。
GO的官方就开发了 context 包,可以轻松地跨API边界将请求范围的值,取消信号和截止日期传递给处理请求的所有goroutine。 该软件包可作为上下文公开使用。
context的解决问题的场景
场景一

使用conext来解决一下,当请求结束后,立马停止正在做的事情
func main() {http.HandleFunc("/hello", func(writer http.ResponseWriter, request *http.Request) {log.Println("我接收到了请求")var wg sync.WaitGroupwg.Add(1)// 做 A 事情go func() {defer wg.Done()select {case <-request.Context().Done(): // 用来判断 这次请求是是否结束了// do somethinglog.Println("我 很快乐的结束了")}}()// 做B事情// 做C事情wg.Wait()})fmt.Println(http.ListenAndServe(":8080", nil))
}
使用context 用来解决 goroutine 之间退出通知、元数据传递的功能(同步请求特定数据、取消信号以及处理请求的截止日期)
context的怎么使用
使用context来 通知子协成
// 使用context来 通知子协成结束
func main() {ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)defer cancel()// 500msgo handle3(ctx, 3*time.Second)// 如果 select中的 任何一个case都没有满足的 则会指定等待下去直到有符合条件的caseselect {case <-ctx.Done():// main context deadline exceeded 代表 是由于超过了过期 时间的导致的错误fmt.Println("main", ctx.Err())}// 避免程序,过早退出导致 子协成的 没有打印出来time.Sleep(time.Second * 1)
}// 使用context 同步信号
func handle3(ctx context.Context, duration time.Duration) {select {case <-ctx.Done():fmt.Println("handle", ctx.Err())case <-time.After(duration):fmt.Println("process request with", duration)}
}
使用context 来向 子协成传递数据(key一定是可以比较的)
func main() {ctx, cancel := context.WithCancel(context.Background())valueCtx := context.WithValue(ctx, "traceId", "abc123-abc")go func(ctx context.Context) {select {case <-ctx.Done():fmt.Println(ctx.Value("traceId"))}}(valueCtx)time.Sleep(1 * time.Second)cancel()time.Sleep(1 * time.Second)
}
context包中的 一些重要数据结构
context接口
type Context interface {// 返回 设置的截止日期和 是否 设置deadlineDeadline() (deadline time.Time, ok bool)// 当context 被取消了 或者到了 deadline 时间会返回一个 只读chanDone() <-chan struct{}// 返回被 取消的原因 Err() error// 返回context中的值,它会递归去根据key拿取数据Value(key interface{}) interface{}
}
context对外提供的主要方法
// 用来做为 contex的父context 是一个 empty的Context
func Background() Context {}// 当你还不清楚 用那种context合适的时候,使用他作为一个占位
func TODO() Context {}// 创建一个可以 cancel context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {}// 创建一个带有截止日期的 context
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {}// WithTimeout 底层就是调用 WithDeadline 创建一个有超时时间的context
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {}// 创建一个存储 k-v 对的 context
func WithValue(parent Context, key, val interface{}) Context {}
cancelCtx
type cancelCtx struct {Contextmu sync.Mutex // protects following fieldsdone chan struct{} // 这个类似 一个信号量,用来通知context 需要取消执行了children map[canceler]struct{} // 用来存放 子contexterr error // set to non-nil by the first cancel call
}
疑问解答
context是如何通知子协成结束的?
-
这里以WitchCancel()的举例
1、构建context,并将子context添加到
children map[canceler]struct{}中2、当cancel触发 调用时候,遍历 当前的context的
children,递归的 去cancel,并将 子context的done close这步至关重要(类似一个信号量)3、删除当前节点从父context中
-
context树

// 重点
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// 一旦 c.done close后,对应的context就会 收到信号,根据收到的信号,做其他(提前结束业务流程)操作if c.done == nil {c.done = closedchan} else {close(c.done)}// 循环递归的形式调用 children的去 cancel子contextfor child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err)}c.children = nilc.mu.Unlock()// 移出自己从父context中if removeFromParent {removeChild(c.Context, c)}
}
为什么对外部返回一个cancel呢?
当程序出现panic或者 满足了一定条件 我们可以更灵活去 defer cancel() 或者 cancel() 和通知子context 退出程序,通过使用 defer cancel()避免了
当程序出现了 panic,而无法 取消 子context,从而导致内存泄露(用的是WitchCancel的Context)
canel传true和false的用意和区别?
- 对外提供了 cancel,外部自己触发 为
true或者 因为到了截止时间传递ture(符合了取消条件或明确了要取消,则从父context中移出自己)
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {return &c, func() { c.cancel(true, Canceled) }
}func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {// d小于当前时间了,说明 已经 deadlinedur := time.Until(d)if dur <= 0 {c.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(false, Canceled) }}if c.err == nil {// 启动了一个定时器,dur后就会执行了 cancelc.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }
}
- cancel的 取消 子context逻辑,在取消children中 传递了false
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// 一旦 c.done close后,对应的context就会 收到信号,根据收到的信号,做其他(提前结束业务流程)操作if c.done == nil {c.done = closedchan} else {close(c.done)}for child := range c.children {// 注意这里 传了falsechild.cancel(false, err)}// 上面取消完毕后,将 该context中的 children中置为nilc.children = nilc.mu.Unlock()// 当外部调用了 或者 因为到了 截止日期 会传入true 将自己从 父context中取消自己if removeFromParent {removeChild(c.Context, c)}
}
- 看图分析

-
当 child.cancel(true), err)
1、ctx11 调用cancel(true,err)
2、遍历 ctx11的 children,调用
child.cancel(true, err)3、以递归的形式取消 children, g1,g2,g3,关闭自己的
done将自己的 chidren=nil4、删除自己从 父cotext中
removeChild(c.Context, c)也就是 将 自己(g1,g2,g3)从父(ctx11)context 中 删除5、children遍历完毕 ,将
ctx11.children =nil, 然后将 ctx1删除从父context中 -
当 child.cancel(false, err)
1、ctx11 调用cancel(true,err)
2、遍历 ctx11的 children,调用
child.cancel(false, err)3、以递归的形式取消 children, g1,g2,g3,关闭自己的
done将自己的 chidren=nil4、children遍历完毕 ,将
ctx11.children =nil(完全可以将 g1,g2,g3从children中删除), 然后将 ctx1删除从父context中 -
总结
1、
child.cancel(true), err)比child.cancel(false), err)每次都多执行一步,removeChild(c.Context, c),而c.children =nil完全可以将 子context(g1,g2,g3)从 children中删除2、只有外部调用 cancel 或者 截止日期到了 cancel才会传true
cancel(true), err), 遍历children,传递cancel信号,并删除自己从 父context中
如何判断协成超时呢?
- WithDeadline的做法
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {c.mu.Lock()defer c.mu.Unlock()if c.err == nil {// 在这里会 创建并启动一个定时器,dur时间到了后,就会执行AfterFunc 函数,从而 `c.cancel(true, DeadlineExceeded)`c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded)})}return c, func() { c.cancel(true, Canceled) }
}
- 验证 AfterFunc是创建并启动了一个定时器了
// 2s过后,打印hello
func main() {go func() {// 当你创建 time.AfterFunc 定时器就时候就开始执行了_ = time.AfterFunc(time.Second*2, func() {fmt.Println("hello")})}()time.Sleep(5 * time.Second)
}
如何添加 子context到 父亲的context的 children
func propagateCancel(parent Context, child canceler) {// 当调用 Done是 会对Done做初始化,(顶级Context除外,因为他是一个空的实现)done := parent.Done()if done == nil {return // parent is never canceled}select {case <-done:// parent is already canceledchild.cancel(false, parent.Err())returndefault:}// 得到一个 cancelCtx的,因为只有 cancelCtx 结构体才有 childrenif p, ok := parentCancelCtx(parent); ok {p.mu.Lock()// err 不为nil,则说明 父context 已经取消了if p.err != nil {// parent has already been canceledchild.cancel(false, p.err)} else {// 构造 children map并将 child 挂载到 父context上if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()} else {atomic.AddInt32(&goroutines, +1)// case1 去掉 可能导致 父节点的取消信号 永远传递不到 子节点// case2 去掉 如果父节点一致不取消,那么就会导致这个goroutine 泄露go func() {select {case <-parent.Done():child.cancel(false, parent.Err())case <-child.Done():}}()}
}
timeCtx的cancel如何取消的?
func (c *timerCtx) cancel(removeFromParent bool, err error) {c.cancelCtx.cancel(false, err)if removeFromParent {// Remove this timerCtx from its parent cancelCtx's children.removeChild(c.cancelCtx.Context, c)}c.mu.Lock()/*c := &timerCtx{cancelCtx: newCancelCtx(parent),deadline: d,}propagateCancel(parent, c)dur := time.Until(d)if dur <= 0 {c.cancel(true, DeadlineExceeded) // deadline has already passedreturn c, func() { c.cancel(false, Canceled) }}*/// 上面的情况 就可能导致 timer为nil,所以需要判断 c.timer !=nil// cancel的调用 可能是 外部直接调用,此时timer 还没有触发执行,也有可能是 deadline后,timer触发调用的if c.timer != nil {// 这里 要停止 定时器,外部已经cancel过了,避免因为到了 deadline后,timer触发再次调动 canclec.timer.Stop()c.timer = nil}c.mu.Unlock()
}
parentCancelCtx(parent) 如何才能返回 false???
参考链接
深度解密Go语言之context
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
