Golang-Context标准库源码深扒-cancelCtx类型

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标准库源码深扒-简介&目录-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

动起点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值