context源码
Context接口有四个方法,
Deadline
方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
Done
方法返回一个只读的chan(我感觉不是只读可以写入零值),类型为struct{}
,我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求(即已经调用了cancel函数),我们通过Done
方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。
Err
方法返回取消的错误原因,因为什么Context被取消。
Value
方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的
1 WithCancel 要想控制一个goroutine结束,可以通过cancel()来控制子context来结束该goroutine,WithCancel是context包下的一个函数,不是一个方法,可以直接执行,如果开启的go协程没有执行cancel函数来结束,就会发生内存泄漏
func main() { // WithCancel返回一个子context和一个可以终止该context的函数cancel, // context.Background()是一个空的Context,用于整个Context的根节点, ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { for { select { // ctx实现了Context接口,在下面调用cancel函数的时候,有close(c.done)关闭了Done中生成的chan, // 任何chan关闭后读取它时,都会返回里面类型的零值,这样Done中的chan就起到了关闭goroutine的作用, case <-ctx.Done(): fmt.Println("监控退出,停止了...") return default: fmt.Println("goroutine监控中...") time.Sleep(2 * time.Second) } } }(ctx) time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") cancel() //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second) }
WithCancel源码,
// cancelCtx可以被取消,如果取消,则它的所有child也都被取消, 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 } func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { // 将传入的上下文包装成私有结构体 context.cancelCtx c := newCancelCtx(parent) // 会构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消 // 作用是在 parent 和 child 之间同步取消和结束的信号, // 保证在 parent 被取消时,child 也会收到对应的信号,不会发生状态不一致的问题 // 核心是里面 p.children[child] = struct{}{} 的注册, propagateCancel(parent, &c) // 返回构建的子context的地址和匿名函数, // 新创建一个可取消的 context 节点,返回的 cancelFunc 函数会传入 true。这样做的结果是: // 当调用返回的 cancelFunc 时,会将这个 context 从它的父节点里“除名”,因为父节点可能有很多子节点, // 你自己取消了,所以我要和你断绝关系,对其他人没影响。 return &c, func() { c.cancel(true, Canceled) } } // newCancelCtx returns an initialized cancelCtx. // 返回一个私有的cancelCtx, func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent} } // propagateCancel arranges for child to be canceled when parent is. // propagateCancel当父上下文取消时,它所有子上下文也会被取消, // https://blog.csdn.net/weixin_42416759/article/details/108460970 func propagateCancel(parent Context, child canceler) { // 取出父上下文的chan, done := parent.Done() // 若为空,说明是emptyCtx,不会被取消, if done == nil { return // parent is never canceled } select { case <-done: // // 若父上下文已经取消了,则调用cancel将当前子上下文及其所有的child都取消, child.cancel(false, parent.Err()) return default: } // 确定parent最内层的cancel内部是否实现了cancelCtx,因为cancelCtx实现了cancel接口,cancel里面 // 里面有cancel方法,之后会通过调用该方法来结束goroutine, // 如果是,则把child放入该cancelCtx的child中 if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { // parent has already been canceled child.cancel(false, p.err) } else { // 将子上下文放入父上下文的children中,这样如果父上下文取消,则子上下文也会取消, if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { // 如果不是内部实现的cancelCtx则另外起一个协程 // 一直监听父context是否关闭,如果关闭则关闭child atomic.AddInt32(&goroutines, +1) go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } } // https://studygolang.com/articles/16320?fr=sidebar 之前的版本是用switch匹配类型来实现的, // 这个初始化了一个标记cancelCtxKey实现更加优雅, func parentCancelCtx(parent Context) (*cancelCtx, bool) { done := parent.Done() if done == closedchan || done == nil { return nil, false } // 判断parent是否封装了cancelctx,如果是,则判断是否重写了Done方法,否则直接返回nil false, // 这里的Value方法是不断调用Value实现的, p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) if !ok { return nil, false } // 检查parent(即candelCtx)的done是否被重写了,如果ok为true,说明没有重写,返回true, // 将child加入parent的children列表中,等待parent释放取消信号 // 如果ok为false,则重写了Done,需要另外判断, p.mu.Lock() ok = p.done == done p.mu.Unlock() if !ok { return nil, false } return p, true } // 如果parent里封装了cancelCtx,则最后必定会调用这个返回c,因为传入的key就是cancelCtxKey, // cancelCtxKey只有在这里会用到,如果传入的key是cancelCtxKey,则走到这里才会返回,也就是说 // 一旦传入的是cancelCtxKey,最后返回的一定是cancelCtx, func (c *cancelCtx) Value(key interface{}) interface{} { if key == &cancelCtxKey { return c } return c.Context.Value(key) } // cancel closes c.done, cancels each of c's children, and, if // removeFromParent is true, removes c from its parent's children. // cancel函数的三个作用 // 1 关闭当前goroutine的chan, // 2 遍历当前goroutine的children,递归关闭它的所有子上下文 // 3 removeFromParent为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 if c.done == nil { c.done = closedchan } else { close(c.done) } 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() // removeFormParent只有第一次才为true,之后就没有必要了,为true的意思是当前的子上下文要和它的父上下文断绝关系 // 即删除父上下文children中的子上下文,之后就为false了,没必要再删除了,因为当前子上下文中children中所有goroutine // 都会被删除,所以上面的child.cancel(false,err) 中传的是false, if removeFromParent { // 这里的c就是WithCancel中生成的c := newCancelCtx(parent),c.Context是它的父上下文,c是子上下文, // 这个函数就是要把父上下文中children中的子上下文删除 delete(p.children, child), // 这个是之前用propagateCancel(parent, &c)来注册的, removeChild(c.Context, c) } }
2 WithDeadline 会多传递一个截止时间参数,意味着到了这个时间点(是绝对时间),会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。
WithTimeout 表示是超时自动取消,是多少时间后自动取消Context的意思,(是相对时间),特别注意这里关闭有两种方法,一种是用cancel()去主动结束goroutine(如果goroutine运行结束了,不手动cancel的话,它会等待timeout的时间,才会释放内存),另一种是等超过所设定的
timeout time.Duration后,内部会自动关闭goroutine,这两种情况都会使ctx.Done()返回close的channel,也即都可通过ctx.Done()来监控,哪个先执行都会导致goroutine结束,之所以不在goroutine跑完后自动结束,是因为把cancel的控制权交给了程序员,如果直接结束的话,这个goroutine下所有的goroutine都会结束,
3 利用WithValue方法可以给context附加值,
package main import ( "context" "fmt" "time" ) var key string="name" func main() { ctx, cancel := context.WithCancel(context.Background()) //附加值,将k-v对加到了context中, valueCtx:=context.WithValue(ctx,key,"【监控1】") go watch(valueCtx) time.Sleep(10 * time.Second) fmt.Println("可以了,通知监控停止") cancel() //为了检测监控过是否停止,如果没有监控输出,就表示停止了 time.Sleep(5 * time.Second) } func watch(ctx context.Context) { for { select { case <-ctx.Done(): //取出值,Value是Context接口的方法, fmt.Println(ctx.Value(key),"监控退出,停止了...") return default: //取出值 fmt.Println(ctx.Value(key),"goroutine监控中...") time.Sleep(2 * time.Second) } } }
参考: https://www.flysnow.org/2017/05/12/go-in-action-go-context.html
https://zhuanlan.zhihu.com/p/68792989
context.Context的作用就是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期
1 对与context的取消功能主要是
1> 用ctx.Done()监听,2> 用cancel()主动取消,3> 基于时间取消
https://segmentfault.com/a/1190000022484275
官方案例:https://deepzz.com/post/golang-context-package-notes.html
context学习:https://www.linkinstar.wiki/2019/06/27/golang/source-code/context-code-view/
1 struct{}类型的chan只能接受struct{}{},其余的都不行,
package main import ( "fmt" "time" ) func fun1(m chan int) { time.Sleep(5*time.Second) m <- 4 } func fun2(m chan struct{}) { m <- struct{}{} } func main() { type s struct { name string } c1 := make(chan int) c2 := make(chan struct{}) //k := s{name: "tom"} // 由于c2是struc{}类型的channel,好像只能写入struct{}{},其它类型无法写入 // https://www.jianshu.com/p/7f45d7989f3a //c2 <- struct{}{} go fun1(c1) go fun2(c2) select { case p := <- c1: fmt.Println("c1接受到了值", p, c1) case q := <- c2: fmt.Println("c2接收到了值", q, c2) } }
Done()的用法就是在context对象结束时返回一个channel,瞎写的,
package main import ( "context" "fmt" "time" ) func main() { // 创建一个子节点的context,3秒后自动超时 // WithTimeout()函数接受一个 Context 和超时时间作为参数,即context的过期时间, // 返回其子Context和取消函数cancel ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) go watch(ctx, "监控1") go watch(ctx, "监控2") fmt.Println("现在开始等待8秒,time=", time.Now().Unix()) time.Sleep(8 * time.Second) fmt.Println("等待8秒结束,准备调用cancel()函数,发现两个子协程已经结束了,time=", time.Now().Unix()) // 若不调用cancel函数,到了原先创建Contetx时的超时时间,它也会自动调用cancel()函数, // 即会往子Context的Done通道发送消息 cancel() } // 单独的监控协程 func watch(ctx context.Context, name string) { for { select { // Done()函数 当ctx返回一个channel,若该channel为空,则 case k := <-ctx.Done(): fmt.Println("-----", k) fmt.Println(name, "收到信号,监控退出,time=", time.Now().Unix()) return default: fmt.Println(name, "goroutine监控中,time=", time.Now().Unix()) time.Sleep(1 * time.Second) } } } // 特别注意Done()返回的类型是struct{}类型的chan,这种chan只能在接收struct{}{}或close(chan)的时候才能结束读取, // 也即只有这两种情况才有返回值, //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 //}
https://www.yuque.com/baxiang/go/oy5l3k
https://zhuanlan.zhihu.com/p/165621566
https://blog.csdn.net/ruanhao1203/article/details/80044026
https://studygolang.com/articles/17796
https://zhuanlan.zhihu.com/p/174532117
http://interview.wzcu.com/Golang/%E5%9F%BA%E7%A1%80.html
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/
https://zhuanlan.zhihu.com/p/91559562
https://zhuanlan.zhihu.com/p/68792989