陈大剩博客

Golang 并发编程(八):Golang 中 Context 类型

  • 陈大剩
  • 2025-03-31 23:29:39
  • 24

golang Golang 中 Context 类型
在使用 WaitGroup 值的时候,最好用 “先统一 Add,再并发 Done,最后 Wait” 的标准模式来构建协作流程。

var wg sync.WaitGroup
wg.Add(2)
go func() {
    defer wg.Done()
    fmt.Println("第一个协程")
}()
go func() {
    defer wg.Done()
    fmt.Println("第二个协程")
}()
wg.Wait()

上面例子必须要等到两个协程完毕后才能进行 wg.Done(),这就带来了一个问题:如果不能在一开始就确定执行子任务的 goroutine 的数量,那么使用 WaitGroup 值来协调它们和分发子任务的 goroutine,就是有一定风险的?那应该如何执行?

换句话说:如果上面的两个 goroutine 我们并不知道他什么时候结束(当然现在也不知道),goroutine 也不知道它自己什么时候结束, goroutine 需要我们给它一个信号,告诉他可以结束了,它才能够结束,这种过程应该怎么做呢?

syncChan := make(chan struct{}, 1)
go func() {
    for {
       select {
       case <-syncChan:
          fmt.Println("结束协程。。。")
          return
       default:
          fmt.Println("持续运行中")
       }
    }

}()
time.Sleep(time.Second)
syncChan <- struct{}{}
time.Sleep(time.Second * 2)

看上去我们可以采用 channel 类型,通过类似信号的机制去访问它,但是如果 goroutine 比较多呢?不像当前一样只有一个,比如 goroutine5 个?难道我们写用 5channel 类型吗?接下来我们可以使用 Context 是群发停止操作。

Context

网络请求场景。例如,每个网络请求 Request 都需要启动一个 goroutine 来处理一些操作,而这些 goroutine 可能还会进一步启动其他 goroutine

因此,我们需要一种可以有效跟踪这些 goroutine 的机制,以便对它们进行控制。Golang 语言为我们提供的 Context 就是这样一种机制,它被称为上下文,恰如其分地反映了它在 goroutine 之间的关联和管理功能。

ctx, cancel := context.WithCancel(context.Background())

go func(ctx context.Context) {
    for {
       select {
       case <-ctx.Done():
          fmt.Println("结束协程。。。")
          return
       default:
          fmt.Println("持续运行中")
       }
    }
}(ctx)
time.Sleep(time.Second)
cancel() // 发送通知
time.Sleep(time.Second * 2)

这里我将之前的例子改为 Context 类型,在 goroutine 中,使用 select 调用 <-ctx.Done() 判断是否要结束,如果接受到值的话,就可以返回结束goroutine了;如果接收不到,就会进行自旋操作。

这里 Context 也可以通过信号控制多个 goroutine

ctx, cancel := context.WithCancel(context.Background())
total := 5
for i := 0; i < total; i++ {
    go func(ctx context.Context, i int) {
       for {
          select {
          case <-ctx.Done():
             fmt.Printf("结束协 %d。。。\n", i)
             return
          default:
             fmt.Printf("协程 %d,持续运行中\n", i)
             time.Sleep(1 * time.Second)
          }
       }
    }(ctx, i)
}

time.Sleep(2 * time.Second)
cancel() // 发送通知
time.Sleep(2 * time.Second)

这里通过一个 context 控制了多个 goroutine ,这在多个协程管理中十分方便,如果使用 chan struct{} 类型信号,工序上复杂很多。

Context 接口类型

Context 与其他同步类型不同的是,它不是一结构体类型,也就是说它和其他同步工具不同的是,可以被传播给多个 goroutineContext 类型实际上是一个接口类型。

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}
  • Deadline 方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context 会自动发起取消请求;第二个返回值 ok==false 时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
  • Done方法返回一个只读的 chan,类型为struct{},在goroutine中,如果该方法返回的 chan 可以读取,则意味着 parent context 已经发起了取消请求,我们通过 Done 方法收到这个信号后,就应该做清理操作,然后退出 goroutine,释放资源。
  • Err 方法返回取消的错误原因,因为什么 Context 被取消。
  • Value 方法获取该 Context 上绑定的值,是一个键值对,所以要通过一个 Key 才可以获取对应的值,这个值一般是线程安全的。

Context 接口并不需要我们实现,Golang 内置已经帮我们实现了 2 个,我们代码中最开始都是以这两个内置的作为最顶层的 partent context,衍生出更多的子 Context

type emptyCtx struct{} // 空结构体
type backgroundCtx struct{ emptyCtx }
type todoCtx struct{ emptyCtx }

一个是 Background,主要用于 main 函数、初始化以及测试代码中,作为 Context 这个树结构的最顶层的 Context,也就是根Context

一个是 TODO,它目前还不知道具体的使用场景,如果我们不知道该使用什么 Context 的时候,可以使用这个。

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
}

这几个是继承 Context 接口的方法,可以理解成什么都没有做,相当于一张白纸。Context 接口类型稍有点晦涩难懂,可以先看 Context 继承衍生再回顾这节可能会更有收获。

Context 继承衍生

我们可以把接口层面的空的 BackgroundTODO 作为整个 Context 的父级,类似于 HTML 中的 domContext 父级中又可以嵌套子集 Context,子集又可以继续嵌套 Context ,如果你愿意可以一直嵌套下去(虽然没有什么意义)。Context 包中还包含了四个用于继承 Context 值的函数,即:WithCancelWithDeadlineWithTimeoutWithValue

首先演示 WithTimeout(WithDeadline) 例子

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    t1 := time.Now()
    defer cancel()
    // 睡眠 500
    time.Sleep(time.Millisecond * 100)
    // 子 context
    ctx1, cancel2 := context.WithTimeout(ctx, 1000*time.Millisecond)
    t2 := time.Now()
    defer cancel2()
    // 阻塞等待
    <-ctx1.Done()
    t3 := time.Now()
    fmt.Println(t2.Sub(t1).Milliseconds(), t3.Sub(t2).Milliseconds())
}

输出结果如下:

100 499 context deadline exceeded

这个例子中:ctx1 继承 ctx ,其中父类级 ctx 使用的 WithTimeout600*time.Millisecond ,子类 ctx1 使用的 1000*time.Millisecond 所以父类 ctx 会优先到时间,到期的结果为 100+499=599 的时间,其中第三个参数为打印超时时间的效果。

WithTimeoutWithDeadline 基本上一样,主要区别是使用时间类型不一致。

WithValue 类型例子:

func step1(ctx context.Context) context.Context {
    child := context.WithValue(ctx, "name", "陈大剩")
    return child
}

func step2(ctx context.Context) context.Context {
    child := context.WithValue(ctx, "age", "18")
    return child
}
func step3(ctx context.Context) {
    fmt.Printf("name %s \n", ctx.Value("name"))
    fmt.Printf("age %s \n", ctx.Value("age"))
}

func main() {
    parent := context.Background()
    child1 := step1(parent)
    child2 := step2(child1)
    step3(child2)
}

输出结果如下:

name 陈大剩 
age 18 

WithValue 函数在产生新的 Context 值(以下简称含数据的 Context 值)的时候需要三个参数,即:父值、键和值。与“字典对于键的约束”类似,这里键的类型必须是可判等的。Context 类型的 Value 方法就是被用来获取数据的。

在我们调用含数据的 Context 值的 Value 方法时,它会先判断给定的键,是否与当前值中存储的键相等,如果相等就把该值中存储的值直接返回,否则就到其父值中继续查找。

WithCancel 类型例子:

ctx, cancel := context.WithCancel(context.Background())
t0 := time.Now()
go func() {
    time.Sleep(1000 * time.Millisecond)
    cancel()
}()
<-ctx.Done()
t1 := time.Now()
fmt.Println(t1.Sub(t0).Milliseconds(), ctx.Err())

输出结果如下:

1000 context canceled

在上述代码中 Done 方法会返回一个元素类型为 struct{} 的接收通道。不过,这个接收通道的用途并不是传递元素值,而是让调用方去感知“撤销”当前 Context值的那个信号。

一旦当前的 Context 值被撤销,这里的接收通道就会被立即关闭。对于一个未包含任何元素值的通道来说,它的关闭会使任何针对它的接收操作立即结束。

而对应的可撤销的 Context 值也只负责传达信号,它们都不会去管后边具体的“撤销”操作。实际上,我们的代码可以在感知到撤销信号之后,进行任意的操作,Context 值对此并没有任何的约束。

Contextgolang 源码中主要用在 HTTP 请求上,有兴趣可以自行去查看。

最后总结一下 Context 使用原则

  1. 不要把 Context 放在结构体中,要以参数的方式传递;
  2. Context 作为参数的函数方法,应该把 Context 作为第一个参数,放在第一位;
  3. 给一个函数方法传递 Context 的时候,不要传递 nil,如果不知道传递什么,就使用 context.TODO
  4. ContextValue 相关方法应该传递必须的数据,不要什么数据都使用这个传递;
  5. Context 是线程安全的,可以放心的在多个 goroutine 中传递;

Context 实战

  1. 根据上述例子写一个能自动启停的实例;
  2. 写一个定时的取消的实例;
分享到:
0

说点儿什么吧

头像

表情

本站由陈大剩博客程序搭建 | 湘ICP备2023000975号| Copyright © 2017 - 陈大剩博客 | 本站采用创作共用版权:CC BY-NC 4.0

站长统计| 文章总数[127]| 评论总数[11]| 登录用户[26]| 时间点[131]

logo

登入

社交账号登录