golang 通过 context 控制并发的应用场景

golang 里出现多 goroutine 的场景很常见, 最常用的两种方式就是 WaitGroup
Context
, 今天我们了解一下 Context
的应用场景

使用场景

场景一: 多goroutine执行超时通知

并发执行的业务中最常见的就是有协程执行超时, 如果不做超时处理就会出现一个僵尸进程, 这累计的多了就会有一阵手忙脚乱了, 所以我们要在源头上就避免它们
看下面这个示例:

package main

import (
    "context"
    "fmt"
    "time"
)

/**
同一个content可以控制多个goroutine, 确保线程可控, 而不是每新建一个goroutine就要有一个chan去通知他关闭
有了他代码更加简洁
*/

func main() {
    fmt.Println("run demo \n\n\n")
    demo()
}

func demo() {
    ctx, cancel := context.WithTimeout(context.Background(), 9*time.Second)
    go watch(ctx, "[线程1]")
    go watch(ctx, "[线程2]")
    go watch(ctx, "[线程3]")

    index := 0
    for {
        index++
        fmt.Printf("%d 秒过去了 \n", index)
        time.Sleep(1 * time.Second)
        if index > 10 {
            break
        }
    }

    fmt.Println("通知停止监控")
    // 其实此时已经超时, 协程已经提前退出
    cancel()

    // 防止主进程提前退出
    time.Sleep(3 * time.Second)
    fmt.Println("done")
}

func watch(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("%s  监控退出, 停止了...\n", name)
            return
        default:
            fmt.Printf("%s goroutine监控中... \n", name)
            time.Sleep(2 * time.Second)
        }
    }
}

使用 context.WithTimeout()
给文本流设置一个时间上限, 结合 for+select
去接收消息. 当执行超时,或手动关闭都会给 <-ctx.Done()
发送消息,

而且 所有
使用同一个 context 都会收到这个通知, 免去了一个一个通知的繁琐代码

场景二: 类似web服务器中的session

比如在php中(没用swoole扩展), 一个请求进来, 从 $_REQUEST
$_SERVER
能获取到的是有关这一条请求的所有信息, 哪怕是使用全局变量也是给这一个请求来服务的, 是线程安全的

但是 golang 就不一样了, 因为程序本身就能起一个 web sever, 因此就不能随便使用全局变量了, 不然就是内存泄露警告. 但是实际业务当中需要有一个类似session 的东西来承载单次请求的信息, 举一个具体的例子就是: 给每次请求加一个 uniqueID 该如何处理?
有了这个 uniqueID, 请求的所有日志都能带上它, 这样排查问题的时候方便追踪一次请求发生了什么
如下:

func demo2() {
    pCtx, pCancel := context.WithCancel(context.Background())
    pCtx = context.WithValue(pCtx, "parentKey", "parentVale")
    go watch(pCtx, "[父进程1]")
    go watch(pCtx, "[父进程2]")

    cCtx, cCancel := context.WithCancel(pCtx)
    go watch(cCtx, "[子进程1]")
    go watch(cCtx, "[子进程2]")
    fmt.Println(pCtx.Value("parentKey"))
    fmt.Println(cCtx.Value("parentKey"))

    time.Sleep(10 * time.Second)
    fmt.Println("子进程关闭")
    cCancel()
    time.Sleep(5 * time.Second)
    fmt.Println("父进程关闭")
    pCancel()

    time.Sleep(3 * time.Second)
    fmt.Println("done")
}

最开始的 context.WithCancel(context.Background())
context.Background()
就是一个新建的 context, 利用 context 能继承的特性,

可以将自己的程序构建出一个 context 树, context 执行 cancel()
将影响到当前 context 和子 context, 不会影响到父级.

同时 context.WithValue
也会给 context 带上自定义的值, 这样 uniqueID 就能轻松的传递了下去, 而不是一层层的传递参数, 改func什么的
对于 context 很值得参考的应用有:

Context 相关 func 和接口

继承 context 需要实现如下四个接口

type Context interface {
    Deadline() (deadline time.Time, ok bool)

    Done() <-chan struct{}

    Err() error

    Value(key interface{}) interface{}
}

当使用的时候不需要实现接口, 因为官方包里已经基于 emptyCtx
实现了一个, 调用方法有

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

// 这个是最初始的ctx, 之后的子ctx都是继承自它
func Background() Context {
    return background
}

// 不清楚context要干嘛, 但是就得有一个ctx的用这个
func TODO() Context {
    return todo
}

继承用的函数

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
  • WithCancel
    返回一个带 cancel 函数的ctx,
  • WithDeadline
    在到达指定时间时自动执行 cancel()
  • WithTimeout
    WithDeadline
    的壳子, 区别就是这个函数是多少时间过后执行 cancel
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}
  • WithValue
    继承父类ctx时顺便带上一个值