简单理解context | 豆包MarsCode AI刷题

42 阅读19分钟

简单理解context

context

context是Golang并发编程的常用编程模式

为什么用context

并发程序中,有时因为超时、取消操作或者其它异常情况,往往需要我们进行抢占操作或者中断后续操作。比如使用done channel来解决

func main() {
    messages := make(chan int, 10)
    done := make(chan bool)

    defer close(messages)
    // consumer
    go func() {
        ticker := time.NewTicker(1 * time.Second)
        for _ = range ticker.C {
            select {
            case <-done:
                fmt.Println("child process interrupt...")
                return
            default:
                fmt.Printf("send message: %d\n", <-messages)
            }
        }
    }()

    // producer
    for i := 0; i < 10; i++ {
        messages <- i
    }
    time.Sleep(5 * time.Second)
    close(done)
    time.Sleep(1 * time.Second)
    fmt.Println("main process exit!")
}

上述生产者-消费者模型就是如此,定义一个buffer0channel done,子协程负责进行定时任务,若主协程要在某个时刻通知子协程中断任务并退出,就可以通过主协程关闭子协程监听的done channel,这样子协程就可以退出。

这样的做法终究是有限制的,假设我们的主协程有多个任务,并对这些任务有超时管理,而每个任务下又有自己的多个子任务,该任务对自己的那些子任务也有超时控制,那么这些子任务既要感知到主协程的取消信号,也得感受到其父任务的取消信号。

当然如果我们依旧采用done channel的方式,也不是不能实现,我们需要定义两个done channel,让子任务同时监听这两个done channel。这样确实也实现了这个功能,但是如果这些子任务还有子任务,子任务的子任务还有子任务……,层级越深,使用done channel的方式就回越繁琐且混乱。

所以我们需要一种比较优雅的方案来实现这么一种机制:

  • 上层任务取消后,所有下层任务都会被取消

  • 中间某层任务取消后,只会将该任务的下层任务取消,同时不影响上层任务和同级任务

这就是我们为什么要使用context的原因

context

context接口

type Context interface {

    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}
}

在源码中有明确注释指明该接口的四个方法:

Deadline

返回绑定当前context的任务被取消的截止时间

如果没有设定期限,将返回ok == false

对 Deadline 的连续调用返回相同的结果。

Done

返回一个通道,当绑定当前context的工作应被取消时,该通道会关闭。

如果此上下文永远无法被取消,Done 可能返回 nil

Done 的连续调用返回相同的值。

Done 通道的关闭可能是异步发生的,在取消函数返回之后。

WithCancel 安排在调用 cancel 时关闭 Done

WithDeadline 安排在截止时间到期时关闭 Done

WithTimeout 安排在超时到达时关闭 Done

Done 用于在 select 语句中使用:

// Stream 生成值并使用 DoSomething 发送到 out,直到 DoSomething 返回错误或ctx.Done被关闭。
func Stream(ctx context.Context, out chan<- Value) error {
        for {
                v, err := DoSomething(ctx)
                if err != nil {
                        return err
                }
                select {
                case <-ctx.Done():
                        return ctx.Err()
                case out <- v:
                }
        }
}

有关如何使用 Done 通道进行取消的更多示例,请参见 blog.golang.org/pipelines

Err

如果 Done 尚未关闭,Err 返回 nil

如果 Done 已关闭,Err 返回一个非 nil 的错误来说明错误原因:

  • 如果context被取消,返回 Canceled

  • 如果context的截止时间已过,返回 DeadlineExceeded

  • Err 返回非 nil 错误后,对 Err 的连续调用将返回相同的错误。”

Value

Value 返回与此context存储的键值对中当前 key 对应的值,如果没有与 key 关联的值,则返回 nil。对同一 key的连续调用将返回相同的结果。

仅将context value用作跨进程和 API 边界传递的请求范围的数据,而不是用于向函数传递的可选参数。

// Use context values only for request-scoped data that transits

// processes and API boundaries, not for passing optional parameters to

// functions.

一个 key 标识 Context 中的特定值。希望将值存储在 Context 中的函数通常会在全局变量中分配一个 key,然后使用该 key 作为 context.WithValueContext.Value 的参数。key 可以是任何支持相等性比较的类型;包应将 key 定义为未导出的类型,以避免冲突。

定义 Context key 的包应提供类型安全的访问器,以便访问使用该 key 存储的值:

// user 包定义了一个存储在 Context 中的 User 类型。
package user

import "context"

// User 是存储在 Context 中的值的类型。
type User struct {...}

// key 是一个未导出的类型,用于在此包中定义的 keys。
// 这可以防止与其他包中定义的 keys 冲突。
type key int

// userKey 是 Context 中 user.User 值的 key。它是未导出的;客户端使用 user.NewContext 和 user.FromContext,而不是直接使用此 key。
var userKey key

// NewContext 返回一个新的 Context,携带值 u。
func NewContext(ctx context.Context, u *User) context.Context {
        return context.WithValue(ctx, userKey, u)
}

// FromContext 返回存储在 ctx 中的 User 值(如果有)。
func FromContext(ctx context.Context) (*User, bool) {
        u, ok := ctx.Value(userKey).(*User)
        return u, ok
}

emptyCtx

在开始的时候,emptyCtx是一个int类型的变量,在后续版本中被更新为struct{}(好像是1.8版本?查到的说是这个更改是为了避免在内存中占用不必要的空间,因为 struct{} 是一个零大小的类型,能够更有效地表示一个空的上下文。)

// An emptyCtx is never canceled, has no values, and has no deadline.
// It is the common base of backgroundCtx and todoCtx.
type emptyCtx struct{}

func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
        return
}

func (emptyCtx) Done() <-chan struct{} {
        return nil
}

func (emptyCtx) Err() error {
        return nil
}

func (emptyCtx) Value(key any) any {
        return nil
}

type backgroundCtx struct{ emptyCtx }

func (backgroundCtx) String() string {
        return "context.Background"
}

type todoCtx struct{ emptyCtx }

func (todoCtx) String() string {
        return "context.TODO"
}

// Background returns a non-nil, empty [Context]. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
        return backgroundCtx{}
}

// TODO returns a non-nil, empty [Context]. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
        return todoCtx{}
}

通常情况下我们不会直接去使用emptyCtx,而是通过调用BackgroundTODO方法得到的由emptyCtx实例化的两个变量,虽然在实现上这两个context是一样的,但是官方注释中标明了个各自的适用范围:

  • Background 返回一个非nil 的空 [Context]。它永远不会被取消,没有值,也没有截止时间。通常由主函数、初始化和测试使用,并作为传入请求的顶层 Context

  • TODO 返回一个非nil 的空 [Context]。当不确定使用哪个 Context 或者尚不可用(因为周围的函数尚未扩展以接受 Context 参数)时,代码应该使用 context.TODO

综上,我们一般创建的context都是基于Background的,只有当我们不确定使用什么context时才会使用TODO

valueCtx

结构体
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
        Context
        key, val any
}

// stringify tries a bit to stringify v, without using fmt, since we don't
// want context depending on the unicode tables. This is only used by
// *valueCtx.String().
func stringify(v any) string {
        switch s := v.(type) {
        case stringer:
                return s.String()
        case string:
                return s
        }
        return "<not Stringer>"
}

func (c *valueCtx) String() string {
        return contextName(c.Context) + ".WithValue(type " +
                reflectlite.TypeOf(c.key).String() +
                ", val " + stringify(c.val) + ")"
}

func (c *valueCtx) Value(key any) any {
        if c.key == key {
                return c.val
        }
        return value(c.Context, key)
}

valueCtx使用一个Context类型的变量来表示父节点context,所以当前的context继承了父context的全部信息;valueCtx海携带了一组键值对以携带额外信息。

它为该键实现了 Value,并将所有其他调用委托给嵌入的 Context。可以在context链路上去获取到key对应的值,如果当前的context上并不存在需要的key,那么就会沿着context链向上寻找key对应的值,直至根节点。

WithValue
// WithValue 返回一个父context的副本,其中与键关联的值为 val。
// 仅在请求范围内的数据传递进程和 API 时使用 context Values,而不是用于向函数传递可选参数。
// 提供的键必须是可比较的,且不应为 string 或任何其他内置类型,以避免不同包之间发生冲突。
// 使用 WithValue 的用户应为键定义自己的类型。为了避免在分配给 interface{} 时进行内存分配,context key通常具有具体类型 struct{}。或者,导出的context key变量的静态类型应为指针或接口。
func WithValue(parent Context, key, val any) Context {
        if parent == nil {
                panic("cannot create context from nil parent")
        }
        if key == nil {
                panic("nil key")
        }
        if !reflectlite.TypeOf(key).Comparable() {
                panic("key is not comparable")
        }
        return &valueCtx{parent, key, val}
}

WithValue是用来给context添加键值对的,此处并非在原context上直接添加,而是以次context作为父节点,重新创建一个新的valueCtx子节点,以形成一条context链,而获取value的过程便是在这条context链上由尾部向前寻找

img

cancelCtx

结构体
// canceler 是一种可以直接取消的上下文类型。
// 其实现为 *cancelCtx 和 *timerCtx。
type canceler interface {
        //用于取消上下文,可以选择是否从父上下文中移除。
        cancel(removeFromParent bool, err, cause error)
        //返回一个只读的通道,当上下文被取消时,该通道会关闭。
        Done() <-chan struct{}
}

// closedchan 是一个可重用的关闭通道
var closedchan = make(chan struct{})

func init() {
        close(closedchan)
}

// cancelCtx 可以被取消。当取消时,它还会取消所有实现了 canceler 的子context。
type cancelCtx struct {
        Context

        mu       sync.Mutex            // 保护以下字段
        done     atomic.Value          // chan struct{},懒惰创建,由第一次取消调用关闭
        children map[canceler]struct{} // 第一次取消调用时设置为 nil
        err      error                 // 第一次取消调用时设置为非空
        cause    error                 // 第一次取消调用时设置为非空
}

类似于valueCtxcancelCtx中也有一个Context类型变量作为父节点;mu用于保护结构体中的共享数据以防止并发访问;done表示一个channel,用来存储关闭信号;children表示一个存储了当前context节点下的子节点的maperr存储错误信息,而cause存储具体错误原因

cancelCtx实现的方法如下:

// Value 方法返回与给定 key 关联的值。
// 如果 key 是 cancelCtxKey 的指针,返回当前上下文 c。
// 否则,调用父上下文的 Value 方法。
func (c *cancelCtx) Value(key any) any {
        if key == &cancelCtxKey {
                return c
        }
        return value(c.Context, key)
}

// 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{})
}

// Err 方法返回上下文的错误信息。
// 如果上下文尚未取消,返回 nil。
func (c *cancelCtx) Err() error {
        c.mu.Lock()
        err := c.err
        c.mu.Unlock()
        return err
}

// propagateCancel 方法确保当父上下文被取消时,子上下文也会被取消。
// 它设置 cancelCtx 的父上下文。
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
        c.Context = parent

        done := parent.Done()
        if done == nil {
                return // 父对象从未被取消
        }

        select {
        case <-done:
                // 父对象已经取消
                child.cancel(false, parent.Err(), Cause(parent))
                return
        default:
        }

        if p, ok := parentCancelCtx(parent); ok {
                // 父对象是一个 *cancelCtx,或者从其派生
                p.mu.Lock()
                if p.err != nil {
                        // 父对象已经被取消
                        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()
                return
        }

        if a, ok := parent.(afterFuncer); ok {
                // 父对象实现了 AfterFunc 方法
                c.mu.Lock()
                stop := a.AfterFunc(func() {
                        child.cancel(false, parent.Err(), Cause(parent))
                })
                c.Context = stopCtx{
                        Context: parent,
                        stop:    stop,
                }
                c.mu.Unlock()
                return
        }

        goroutines.Add(1)
        go func() {
                select {
                case <-parent.Done():
                        child.cancel(false, parent.Err(), Cause(parent))
                case <-child.Done():
                }
        }()
}

type stringer interface {
        String() string
}

// contextName 函数获取上下文的名称。
// 如果上下文实现了 stringer 接口,调用其 String 方法;
// 否则,使用反射获取类型名。
func contextName(c Context) string {
        if s, ok := c.(stringer); ok {
                return s.String()
        }
        return reflectlite.TypeOf(c).String()
}

// String 方法返回上下文的字符串表示,包含父上下文的名称
func (c *cancelCtx) String() string {
        return contextName(c.Context) + ".WithCancel"
}

// cancel 方法关闭 c.done,取消每个子上下文,
// 并根据 removeFromParent 的值,
// 决定是否从父上下文的子上下文中移除自身。
// 当第一次取消时,设置 c.cause 为 cause。
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
        if err == nil {
                panic("context: internal error: missing cancel error")
        }
        if cause == nil {
                cause = err
        }
        c.mu.Lock()
        if c.err != nil {
                c.mu.Unlock()
                return // 已经取消
        }
        c.err = err
        c.cause = cause
        d, _ := c.done.Load().(chan struct{})
        if d == nil {
                c.done.Store(closedchan)
        } else {
                close(d)
        }
        for child := range c.children {
                // 注意:在持有父对象锁的同时获取子对象的锁。
                child.cancel(false, err, cause)
        }
        c.children = nil
        c.mu.Unlock()

        if removeFromParent {
                removeChild(c.Context, c)
        }
}

可以发现cancelCtx类型变量其实也是canceler类型,因为cancelCtx实现了canceler接口。Done方法和Err方法没必要说了,cancelCtx类型的context在调用cancel方法时会设置取消原因,将done channel设置为一个关闭channel或者关闭channel,然后将子节点context依次取消,如果有需要还会将当前节点从父节点上移除。

propagateCancel放到WithCancel中一起说

WithCancel

WithCancel函数用来创建一个可取消的context,即cancelCtx类型的contextWithCancel返回一个context和一个CancelFunc,调用CancelFunc即可触发cancel操作。

// CancelFunc 用于指示某个操作放弃其工作。
// CancelFunc 不会等待工作停止。
// CancelFunc 可以被多个 goroutine 同时调用。
// 在第一次调用后,对 CancelFunc 的后续调用将无效。
type CancelFunc func()

// WithCancel 返回一个父context的副本,并带有一个新的 Done 通道。
// 返回的context的 Done 通道在调用返回的取消函数时关闭,
// 或者当父context的 Done 通道关闭时,以先发生者为准。
// 取消此context会释放与之相关的资源,因此代码应在此context中运行的操作完成后尽快调用 cancel。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        c := withCancel(parent)
        return c, func() { c.cancel(true, Canceled, nil) }
}

// CancelCauseFunc 的行为类似于 [CancelFunc],但额外设置了取消原因。
// 这个原因可以通过在被取消的context或其任何派生context上调用 [Cause] 来检索。
// 如果context已经被取消,CancelCauseFunc 不会设置原因。
//
// 例如,如果 childContext 是从 parentContext 派生的:
//   - 如果 parentContext 在 childContext 被 cause2 取消之前被 cause1 取消,
//     则 Cause(parentContext) == Cause(childContext) == cause1
//   - 如果 childContext 在 parentContext 被 cause1 取消之前被 cause2 取消,
//     则 Cause(parentContext) == cause1 且 Cause(childContext) == cause2
type CancelCauseFunc func(cause error)

// WithCancelCause 的行为类似于 [WithCancel],但返回一个 [CancelCauseFunc] 而不是 [CancelFunc]。
// 调用 cancel 时传入非空error(“cause”)会将该错误记录在 ctx 中;
// 然后可以使用 Cause(ctx) 检索该错误。
// 调用 cancel 时传入 nil 会将原因设置为 Canceled。
//
// 示例用法:
//
//        ctx, cancel := context.WithCancelCause(parent)
//        cancel(myError)
//        ctx.Err() // 返回 context.Canceled
//        context.Cause(ctx) // 返回 myError
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
        c := withCancel(parent)
        return c, func(cause error) { c.cancel(true, Canceled, cause) }
}

func withCancel(parent Context) *cancelCtx {
        if parent == nil {
                panic("cannot create context from nil parent")
        }
        c := &cancelCtx{}
        c.propagateCancel(parent, c)
        return c
}

// parentCancelCtx 返回父context的底层 *cancelCtx。
// 它通过查找 parent.Value(&cancelCtxKey) 来找到
// 最内部的 *cancelCtx,然后检查 parent.Done() 是否与该 *cancelCtx 匹配。
// (如果不匹配,则说明 *cancelCtx 已被包装在一个自定义实现中,提供了
// 不同的完成通道,在这种情况下,我们不应该绕过它。)
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
        done := parent.Done()
        if done == closedchan || done == nil {
                return nil, false
        }
        p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
        if !ok {
                return nil, false
        }
        pdone, _ := p.done.Load().(chan struct{})
        if pdone != done {
                return nil, false
        }
        return p, true
}

之前说到cancelCtx取消时,会将后代节点中所有的cancelCtx都取消,propagateCancel即用来建立当前节点与祖先节点这个取消关联逻辑。

done := parent.Done()
if done == nil {
    return // 父对象从未被取消
}

1.如果parent.Done()返回nil,表明父节点以上的路径上没有可取消的context,不需要处理;

select {
case <-done:
    // 父对象已经取消
    child.cancel(false, parent.Err(), Cause(parent))
    return
default:
}

2.使用 select 来监听父contextDone 通道:

  • 如果父context已被取消(即通道关闭),则调用child.cancel()方法,传递错误信息和原因,并返回。

  • 如果父context尚未取消,则继续执行后面的代码。

if p, ok := parentCancelCtx(parent); ok {
    // 父对象是一个 *cancelCtx,或者从其派生
    p.mu.Lock()
    if p.err != nil {
        // 父对象已经被取消
        child.cancel(false, p.err, p.cause)
    } else {
        if p.children == nil {
            p.children = make(map[canceler]struct{})
        }
        // 将当前子节点加入最近cancelCtx祖先节点的children中
        p.children[child] = struct{}{}
    }
    p.mu.Unlock()
    return
}

3.检查父context是否是 *cancelCtx 类型:(获取最近的类型为cancelCtx的祖先节点)

  • 如果是,则获取该context

  • 锁定p.mu以保护共享资源。

  • 如果父context已经被取消(p.err 不为 nil),则调用 child.cancel()

  • 如果父context还未被取消,将 child 添加到p.children中,以便在父context取消时能够通知所有子context

  • 解锁 p.mu

if a, ok := parent.(afterFuncer); ok {
    // 父对象实现了 AfterFunc 方法
    c.mu.Lock()
    stop := a.AfterFunc(func() {
        child.cancel(false, parent.Err(), Cause(parent))
    })
    c.Context = stopCtx{
        Context: parent,
        stop:    stop,
    }
    c.mu.Unlock()
    return
}

4.检查父context是否实现了 AfterFunc 接口:

  • 如果实现了,则调用其 AfterFunc 方法注册一个回调。

  • 在回调中,当父context被取消时,调用 child.cancel()

  • 创建一个 stopCtx 对象,包含父context和停止函数,并将其赋值给当前context

goroutines.Add(1)
go func() {
    select {
    case <-parent.Done():
        child.cancel(false, parent.Err(), Cause(parent))
    case <-child.Done():
    }
}()

5.启动一个新的 goroutine

  • 监听父contextDone 通道。

  • 如果父context被取消,调用 child.cancel() 方法。

  • 也监听 child.Done(),如果子context先被取消,则不做任何操作。

这里有个问题,为什么寻找的是祖先节点而不直接是父节点

若当前cancelCtx的父节点并非一个可取消的context,它就无法记录children

timerCtx

timerCtx是一种基于cancelCtxcontext类型,显而易见的是:这是一种可以定时取消的context

// timerCtx 包含一个定时器和一个截止时间。
//它嵌入了一个 cancelCtx,以实现 Done 和 Err。
//它通过停止定时器来实现取消,然后委托给 cancelCtx.cancel。
type timerCtx struct {
        cancelCtx
        timer *time.Timer // Under cancelCtx.mu.

        deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
        return c.deadline, true
}

//String 方法返回 timerCtx 的字符串表示,包含上下文名称和截止时间,以及距离截止时间的剩余时间。
func (c *timerCtx) String() string {
        return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
                c.deadline.String() + " [" +
                time.Until(c.deadline).String() + "])"
}

func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
        //取消内部的cancelCtx
        c.cancelCtx.cancel(false, err, cause)
        if removeFromParent {
                // 从父 cancelCtx 的子级中移除自己
                removeChild(c.cancelCtx.Context, c)
        }
        c.mu.Lock()
        if c.timer != nil {
                //取消计时器
                c.timer.Stop()
                c.timer = nil
        }
        c.mu.Unlock()
}

timerCtx内部使用cancelCtx实现取消,另外使用定时器timer和过期时间deadline实现定时取消的功能。timerCtx在调用cancel方法,会先将内部的cancelCtx取消,如果需要则将自己从cancelCtx祖先节点上移除,最后取消计时器。

WithDeadline
// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// [Context.Done] channel is closed when the deadline expires, 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 WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
        return WithDeadlineCause(parent, d, nil)
}

// WithDeadlineCause behaves like [WithDeadline] but also sets the cause of the
// returned Context when the deadline is exceeded. The returned [CancelFunc] does
// not set the cause.
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
        if parent == nil {
                panic("cannot create context from nil parent")
        }
        //检查父上下文是否有截止时间,并判断当前截止时间 cur 是否早于新的截止时间 d。如果是,则返回一个使用 WithCancel 创建的新上下文,因为没有必要设置新的截止时间。
        if cur, ok := parent.Deadline(); ok && cur.Before(d) {
                // The current deadline is already sooner than the new one.
                return WithCancel(parent)
        }
        //创建一个 timerCtx 实例,并将截止时间设置为 d。调用 propagateCancel 方法,以确保取消操作能够从父上下文传播到新上下文。
        c := &timerCtx{
                deadline: d,
        }
        // 建立新建context与可取消context祖先节点的取消关联关系
        c.cancelCtx.propagateCancel(parent, c)
        //计算截止时间与当前时间的持续时间 dur。如果 dur 小于或等于 0,说明截止时间已经过去,立即调用 cancel 方法以标记为 DeadlineExceeded。然后返回上下文和取消函数。
        dur := time.Until(d)
        if dur <= 0 {
                c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
                return c, func() { c.cancel(false, Canceled, nil) }
        }
        c.mu.Lock()
        defer c.mu.Unlock()
        if c.err == nil {
                //创建一个定时器,在 dur 时间后调用 cancel 方法,以处理截止时间到期的情况。
                c.timer = time.AfterFunc(dur, func() {
                        c.cancel(true, DeadlineExceeded, cause)
                })
        }
        return c, func() { c.cancel(true, Canceled, nil) }
}
WithTimeout
// WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。
//
// 取消此上下文将释放与之相关的资源,因此代码应在此 [Context] 中运行的操作完成后尽快调用取消函数:
//
//        func slowOperationWithTimeout(ctx context.Context) (Result, error) {
//                ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
//                defer cancel()  // 如果 slowOperation 在超时时间之前完成,释放资源
//                return slowOperation(ctx)
//        }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
        return WithDeadline(parent, time.Now().Add(timeout))
}

WithDeadline类似,WithTimeout也是创建一个定时取消的context,只不过WithDeadline是接收一个过期时间点,而WithTimeout接收一个相对当前时间的过期时长timeout

context的使用

使用context实现文章开头done channel的例子来示范一下如何更优雅实现协程间取消信号的同步

func main() {
        messages := make(chan int, 10)

        // producer
        for i := 0; i < 10; i++ {
                messages <- i
        }

        ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

        // consumer
        //让子线程监听主线程传入的ctx,一旦ctx.Done()返回空channel,子线程即可取消执行任务
        go func(ctx context.Context) {
                ticker := time.NewTicker(1 * time.Second)
                for _ = range ticker.C {
                        select {
                        case <-ctx.Done():
                                fmt.Println("child process interrupt...")
                                return
                        default:
                                fmt.Printf("send message: %d\n", <-messages)
                        }
                }
        }(ctx)

        defer close(messages)
        defer cancel()

        select {
        case <-ctx.Done():
                time.Sleep(1 * time.Second)
                fmt.Println("main process exit!")
        }
}

net/http中实现http server时就有使用context

  • 首先Server在开启服务时会创建一个valueCtx,存储了server的相关信息,之后每建立一条连接就会开启一个协程,并携带此valueCtx
func (srv *Server) Serve(l net.Listener) error {

    ...

    var tempDelay time.Duration     // how long to sleep on accept failure
    baseCtx := context.Background() // base is always background, per Issue 16220
    ctx := context.WithValue(baseCtx, ServerContextKey, srv)
    for {
        rw, e := l.Accept()

        ...

        tempDelay = 0
        c := srv.newConn(rw)
        c.setState(c.rwc, StateNew) // before Serve can return
        go c.serve(ctx)
    }
}
  • 建立连接之后会基于传入的context创建一个valueCtx用于存储本地地址信息,之后在此基础上又创建了一个cancelCtx,然后开始从当前连接中读取网络请求,每当读取到一个请求则会将该cancelCtx传入,用以传递取消信号。一旦连接断开,即可发送取消信号,取消所有进行中的网络请求。
func (c *conn) serve(ctx context.Context) {
    c.remoteAddr = c.rwc.RemoteAddr().String()
    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())
    ...

    ctx, cancelCtx := context.WithCancel(ctx)
    c.cancelCtx = cancelCtx
    defer cancelCtx()

    ...

    for {
        w, err := c.readRequest(ctx)

        ...

        serverHandler{c.server}.ServeHTTP(w, w.req)

        ...
    }
}
  • 读取到请求之后,会再次基于传入的context创建新的cancelCtx,并设置到当前请求对象req上,同时生成的response对象中cancelCtx保存了当前context取消方法。
func (c *conn) readRequest(ctx context.Context) (w *response, err error) {

    ...

    req, err := readRequest(c.bufr, keepHostHeader)

    ...

    ctx, cancelCtx := context.WithCancel(ctx)
    req.ctx = ctx

    ...

    w = &response{
        conn:          c,
        cancelCtx:     cancelCtx,
        req:           req,
        reqBody:       req.Body,
        handlerHeader: make(Header),
        contentLength: -1,
        closeNotifyCh: make(chan bool, 1),

        // We populate these ahead of time so we're not
        // reading from req.Header after their Handler starts
        // and maybe mutates it (Issue 14940)
        wants10KeepAlive: req.wantsHttp10KeepAlive(),
        wantsClose:       req.wantsClose(),
    }

    ...
    return w, nil
}

这样处理的目的主要有以下几点:

  • 一旦请求超时,即可中断当前请求;
  • 在处理构建response过程中如果发生错误,可直接调用response对象的cancelCtx方法结束当前请求;
  • 在处理构建response完成之后,调用response对象的cancelCtx方法结束当前请求。

在整个server处理流程中,使用了一条context链贯穿ServerConnectionRequest,不仅将上游的信息共享给下游任务,同时实现了上游可发送取消信号取消所有下游任务,而下游任务自行取消不会影响上游任务。

说在最后
  • 上游任务仅仅使用context通知下游任务不再需要,但不会直接干涉和中断下游任务的执行,由下游任务自行决定后续的处理操作,也就是说context的取消操作是无侵入的;
  • context是线程安全的,因为context本身是不可变的(immutable),因此可以放心地在多个协程中传递使用。