Golang 并发模式Context

context 在 http 服务, rpc 服务等功能中经常被用到。

在 golang 并发编程中,channel 被用来在 goroutine 之间共享数据, 而 context 主要被用来保存一个 goroutine 和它创建的 goroutine 之间的上下文管理。可以用来传递变量和提供超时等功能。

context 的接口定义

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

该接口定义了四个函数来观察 context 的状态

  • Done 返回一个当 context 被取消或者结束时候会被关闭的 channel
  • Deadline 如果 context 被设置了过期时间,者返回该时间 如果没有定义该过期, 返回false
  • Err 当 Done 返回的 channel 关闭后,Err 返回 non-nil 的 错误值。如果被取消返回 Canceled , 如果过期返回 DeadlineExceeded
  • Value 返回和 context 相关联的值

默认 context

context 包中提供了两个默认的 package

var (
    todo       = context.TODO()
    background = context.Background()
)
  • context.TODO 当不清楚该使用哪一个 context 的时候,可以使用 context.TODO
  • BackGround 通常在初始化函数、main 函数等高层的的地方被使用

两个 context 都没有取消、没有传值,也没有过期功能

其他 context

context包提供了四种基础的 context

  • context.WithCancel 返回一个 从 parent context 继承的 context。 如果 parent context 的 Done 返回的channel 别关闭或 CancelFunc 被调用的时候,该 context 的 Done 返回的 channel 也会被关闭。

    func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
        ctx, f := context.WithCancel(parent)
        return ctx, CancelFunc(f)
    }
    
  • context.WithTimeout 返回一个 context。 当该 context 的 CancelFunc 被调用,或者 context 到期,或者 parent context 的 Done Channel 关闭,都会导致该 context 的Done channel 关闭

    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
        return WithDeadline(parent, time.Now().Add(timeout))
    }
    
  • context.WithDeadline 和 WithTimeout 功能类似

    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
        ctx, f := context.WithDeadline(parent, deadline)
        return ctx, CancelFunc(f)
    }
    
  • context.WithValue 返回的 context 有一个 key-value map,该 context 及其子 goutine 都能使用该 map

    func WithValue(parent Context, key interface{}, val interface{}) Context {
        return context.WithValue(parent, key, val)
    }
    

example

下面的一个例子中包含了 context 基本用法

package main

import (
    "bufio"
    "fmt"
    "strings"
    "time"
    "context"
)

// ctx 一般用作第一个参数
func Inc(ctx context.Context, a int) {

    // 此处用 ctx 的 value 来获取 一个值。 不过 ctx 的 value 通常情况下不用来传递参数。
    // 此处只是用来说明 context.Value 的用法
    intival := ctx.Value("interval").(time.Duration)

    for {
        select {
        // 当子 ctx 的 cancelfunc 被调用的时候或者 context 到期, Done 关闭 
        case <-ctx.Done():
            if ctx.Err() == context.Canceled {     // ctx 的 cancel 被调用
                fmt.Println("function canceled")
                return
            } else if ctx.Err() == context.DeadlineExceeded {
                // WithDeadline 或 WithTimeout 设定的超时被触发
                fmt.Println("function time out ")
                return
            }
        default:
            time.Sleep(intival)
            a++
        }
    }
}

func main() {
    dura := time.Second * 1
    ctx := context.WithValue(context.Background(), "interval", dura)
    ctx, cancel := context.WithTimeout(ctx, 1*time.Minute)
    // 先从 Background context 继承了一个带 value 的 context,再继承了一个带超时的 ctx。

    go Inc(ctx, 0) // 新开一个 goutine, 传入当前 goroutine 的上下文  
    // context 是 goroutine 安全的, 可以在多个 goroutine 中同时访问该 context

    fmt.Print("Inc function is runnning")
    if deadline, ok := ctx.Deadline(); ok {
        fmt.Printf(" %s second left to run the Inc function .\n", deadline.Sub(time.Now()).String())
    }
    // ctx.Deadline 主要获取ctx 的过期时间,如果没有设置超时的话 ok 会返回 false
    reader := bufio.NewReader(os.Stdin)
    for {

        fmt.Printf("  Press A to abort: ")
        if line, err := reader.ReadString('\n'); err == nil {
            command := strings.TrimSuffix(line, "\n")
            switch command {
            case "A":
                cancel()        // 手动调用 ctx 的cancelfunc ctx Done 返回的 channel 会关闭
                time.Sleep(500 * time.Millisecond)
                return
            default:
                if deadline, ok := ctx.Deadline(); ok {
                    fmt.Printf("%s second left, Press A to stop `\n", deadline.Sub(time.Now()).String())
                }
            }
        }
    }
}

总结

遇到 context 了比较长一段时间了,之前一直看不明白。 这次用下心来发现也没有那么困难, 先尝试去理解,理解知乎去试着写一个 demo。 这样理解就会加深很多。然后,现在看的代码是在太少了,继续加油吧。

参考文档

如果你觉得本文对你有帮助,欢迎打赏!