cancelCtx类型
cancelCtx类型实现了Context接口,用于取消信号的发送(通知所有使用该Context的goroutine停止执行某些操作,但是停止操作的逻辑是自己给出的,cancelCtx只是用来通知的),通常是由context.WithCancel()的创建方法生成的。
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
Context
mu sync.Mutex //使用互斥锁来保护以下字段
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
cause error // set to non-nil by the first cancel call
}
例如:
func reqTask(ctx context.Context, name string) {
for {
select {
case <-ctx.Done()://取消的信号,ctx.Done()不断返回channel struct{}来阻塞case,如果执行cancel(),则 ctx.Done()返回的通道将关闭,而如果通道已经没有更多数据,并且被关闭了,接收操作将 接收到零值 并且 不会阻塞,这会导致执行后续停止操作的代码,由此来控制goroutine的停止
fmt.Println("stop", name)//停止操作的逻辑
return
default:
fmt.Println(name, "send request")
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go reqTask(ctx, "worker1")
time.Sleep(3 * time.Second)
cancel()
time.Sleep(3 * time.Second)
}
canceler接口
同时,cancelCtx实现了canceler接口。
canceler接口就是所有cancelCtx类型都要实现的接口。
type canceler interface {
cancel(removeFromParent bool, err, cause error)
Done() <-chan struct{}
}
其中,cancel(removeFromParent bool, err, cause error)方法就是WithCancel()返回的方法,使得我们可以在外部调用cancel方法,让其控制的goroutine停止。
而Done() <-chan struct{}方法是用于监听该context是否取消的。
mu sync.Mutex
使用互斥锁来保护以下字段
done atomic.Value
存储空结构体的通道,用于判断该cancelCtx控制的上下文是否开始运行(是否开始监听),同时用来发送取消信号。
当done中存储的chan struct{}在cancel中被close()后,表明操作停止,发送取消信号。
在cancelCtx.Done()中,若done为空(第一次开始监听),则存储空结构体的通道进入done中,然后返回空结构体的类型断言;若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{})
}
而在cancelCtx.cancel()中,若done为空,则代表监听未开始,则在其中存储一个关闭的通道closedchan,当其开始监听,则自动发送取消信号;若done不为空,则直接close(done中存储的通道)。
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
//...
c.mu.Lock()
//...
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
//...
c.mu.Unlock()
//...
}
children map[canceler]struct{}
存储该cancelCtx的子节点的set
在创建一个cancelCtx时,会向上寻找父节点,若找到类型是cancelCtx的父节点,则将自己加入到父节点的children的set中,在propagateCancel()中实现。
func propagateCancel(parent Context, child canceler) {
//...
if p, ok := parentCancelCtx(parent); ok {//向上寻找类型是cancelCtx的父节点,若找到,则返回其父节点和true;否则返回 nil和false
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err, p.cause)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
//...
}
}
在删除一个cancelCtx时,直接cancel其children的set中的全部子节点,并且删除其父节点的children的set中的自己,在cancel()中实现。
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
//...
c.mu.Lock()
//...
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err, cause)
}
c.children = nil
c.mu.Unlock()
//删除其父节点的children的set中的自己
if removeFromParent {
removeChild(c.Context, c)
}
}
err Error & cause Error
删除后,给出删除的原因。
err还可以验证该cancelCtx是否被删除,若未被删除err应为nil。
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
//...
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
c.cause = cause
//...
c.mu.Unlock()
//...
}
cancelCtx运行步骤
创建:初始化 ; 传播父节点
工作:监听done
删除:err赋值 ; 关闭通道 ; 调用所有子节点的cancel方法 ; 删除与父节点的关系
创建
初始化
在外部使用context.WithCancel()或context.WithCancelCause()函数,获取cancelCtx和cancel函数
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
在内部,根据parent创建一个新的cancelCtx
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, c)//传播到父节点
return c
}
func newCancelCtx(parent Context) *cancelCtx {
return &cancelCtx{Context: parent}
}
传播父节点
若最近的父节点为emptyCtx则直接返回(因为emptyCtx永远不会cancel)
若最近的父节点已经被关闭,则直接cancel子节点
若能找到类型为cancelCtx的父节点,就再次检查父节点是否被关闭(因为是并发编程),然后将自己加入父节点的children的set中
若不能找到类型为cancelCtx的父节点,则启动一个goroutine来监听父节点是否cancel,若cancel,则该context也随之cancel
var goroutines atomic.Int32
func propagateCancel(parent Context, child canceler) {
//若最近的父节点为emptyCtx则直接返回
done := parent.Done()
if done == nil {
return // parent is never canceled
}
//若最近的父节点已经被关闭,则直接cancel子节点
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err(), Cause(parent))
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
//若能找到类型为cancelCtx的父节点
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err, p.cause)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
//若不能找到类型为cancelCtx的父节点
goroutines.Add(1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
}
}
工作
监听done
使用cancelCtx.Done()方法,上面已经叙述。
删除
err赋值
对cancelCtx的err字段进行赋值,写明cancel的Error信息,并且表明该cancelCtx已被删除。
关闭通道
若done为空,则代表监听未开始,则在其中存储一个关闭的通道closedchan,当其开始监听,则自动发送取消信号;若done不为空,则直接close(done中存储的通道)。
调用所有子节点的cancel方法
父节点关闭,则所有子节点全部关闭
删除与父节点的关系
找到父节点,然后在其children的set中删除自己
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
本次分享结束,本专栏将持续更新有关golang源码的内容,欢迎大家关注
如果想学习更多Context库相关内容:Golang-Context标准库源码深扒-简介&目录-优快云博客