深入理解 Context

概览

Golang 的 context 包很多时候被用来处理 Goroutine 链的数据共享,退出等操作。

这里主要描述 Context 的使用示例和内部实现。

特性

  • Context 是并发安全的。

  • 支持树状的上级控制下级,不支持反向控制和平级控制。

接口

type Context interface{
    Deadline() (deadline time.Time, ok bool) // 返回被取消的时间
    Done() <-chan struct{} // 返回一个 Channel,当任务完成或被取消时这个 Channel 会被关闭,多次调用 Done 会返回同一个 Channel
    Err() error // 返回 Context 结束的原因
    Value(key interface{}) interface{}
}

官方示例

package main
import (
        "context"
        "fmt"
)

func main() {
        gen := func(ctx context.Context) <-chan int {
                dst := make(chan int)
                n := 1
                go func() {
                        for {
                                select {
                                case <-ctx.Done():
                                        return // returning not to leak the goroutine
                                case dst <- n:
                                        n++
                                }
                        }
                }()
                return dst
        }

        ctx, cancel := context.WithCancel(context.Background())
        defer cancel() // cancel when we are finished consuming integers

        for n := range gen(ctx) {
                fmt.Println(n)
                if n == 5 {
                        break
                }
        }
}

CancelContext 的内部实现

Context 的实现

context 本质上是一个这样的接口

type Context interface{
        Deadline() (deadline time.Time, ok bool)
        Done() <- chan struct{}
        Err() error
        Value(key interface{}) interface{}
}

分别包含了生存信号,取消信号,Goroutine 之间共享的值。

包内针对这个接口实现了 emptyCtx,返回值都是零值。

CancelCtx

创建 cancelCtx 实例主要做了一下几件事。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        c := newCancelCtx(parent) // new 一个 cancelCtx 实例
        propagateCancel(parent, &c) // 在父节点上挂载 cancel 信息
        return &c, func() { c.cancel(true, Canceled) } //什么意思,看下文您就明白了
}

propagateCancel 这个方法如下

func propagateCancel(parent Context, child canceler) {
        // 父节点是一个空节点,可以理解为本节点为根节点,不需要挂载
        if parent.Done() == nil {
                return // parent is never canceled
        }
        // 父节点是可取消类型的
        if p, ok := parentCancelCtx(parent); ok {
                p.mu.Lock()
                if p.err != nil {
                        // parent has already been canceled
                        // 父节点被取消了,本节点也需要取消
                        child.cancel(false, p.err)
                } else {
                        if p.children == nil {
                                p.children = make(map[canceler]struct{})
                        }
                        // 挂载到父节点
                        p.children[child] = struct{}{}
                }
                p.mu.Unlock()
        } else {
                // 为了兼容,Context 内嵌到一个类型里的情况发生
                go func() {
                        select {
                        case <-parent.Done():
                                child.cancel(false, parent.Err())
                        case <-child.Done():
                        }
                }()
        }
}

cancel 的方法实现如下

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 
        }
        // 设置 cancelCtx 错误信息
        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()

        if removeFromParent {
                // 从父节点中移除自己 
                removeChild(c.Context, c)
        }
}

主要工作:

  • 设置错误信息

  • 关闭 channel

  • 递归的取消子节点

  • 从父节点中移除自己

参考

深入 Go 并发模型:Context
golang从context源码领悟接口的设计