本文基于golang 1.17对Golang 带有取消功能的Context的实现进行学习,了解其实现取消操作的实现。
开始之前我们先上一段简单的代码来看看效果。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.TODO()
cancel, cancelFunc := context.WithCancel(ctx)
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
fmt.Println("end")
return
}
}
}(cancel)
cancelFunc()
time.Sleep(time.Second * 3)
}
这段代码做了以下几件事情
- 创建一个没有具体实现的context context.TODO()函数返回没有具体实现的context,第10行
- 创建出带有取消功能的context,第11行
- 创建一个goroutines,并监听context是否被取消,第12~21 行
- 取消context,第22行
- 休眠3秒,第23行
最后的输出应该是:
context canceled
end
接下来我们来看看这个功能是怎么实现的。通过的上面的代码我们知道取消context这个动作是由函数cancelFunc()触发的,进入context.WithCancel()看看这个函数的具体实现
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
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) }
}
函数将传入的context作为父级创建了一个新的子级context,然后返回新创建的context和一个CancelFun类型的函数,通过最开始的演示代码知道就是这个函数出发了取消的context的动作。在查看取消函数之前我们先看看新创建出的context的结构是什么
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
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
}
知道了cancelCtl的包含哪些字段之后进一步看看是如何取消context的
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
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)
} else {
close(d)
}
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)
}
}
这是具体的取消函数,这个函数的主要作用就是向done中放入一个空的结构体,关闭chan d 第17行,然后依次取消当前context的子context,
接下来我们来看看context是怎么通过Done()得到的取消通知
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
通过阅读代码这个函数只是到done中的取值,如果没有就创建一个chan struct {},并加载到done。所有chan struct的作用只是一个占位的作用,当done中的chan 被关闭的的时候我们自然就可以知道context已经被取消。
在取消函数里我们看见
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
这段代码,closedchan 是一个已经关闭的chan
var closedchan = make(chan struct{})
func init() {
close(closedchan)
}
所以如果在执行取消函数的时候done里没有值的话就直接加载一个已经关闭的chan。
带有取消功能context可以让我们控制创建的goroutines结束自己。
欢迎指正文章中的错误