通过上下文协作 — 详解 golang 中的 context

1. 引言

通过 golang 中的 goroutine 实现并发编程是十分简单方便的,此前我们进行了非常详细的介绍,并且看到了如何通过 channel 来协调多个并发的 goroutine。
GoLang 的并发编程与通信 — goroutine 与通道

channel 本质上是用来在多个 goroutine 之间进行数据传输的通道,用它来进行 goroutine 并发的协调看起来有些繁琐。
golang 1.7 引入了 Context,可以十分方便的实现:

  1. 多个 goroutine 之间数据共享和交互

  2. goroutine 超时控制

  3. 运行上下文控制

本文我们就来详细介绍一下 golang 中的 context 的使用。

2. Context 接口

golang 中 Context 本质上是一个接口:

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

他声明了四个方法:

  1. Deadline() — 返回 bool 类型的 ok 表示是否定义了超时时间,time.Time 类型的 deadline 则表示 context 应该结束的时间

  2. Done() — 返回一个 channel,当到达 deadline 所定义的时间或 context 被取消时,返回的 channel 会传递一个信号

  3. Err() — 返回 context 被取消的原因

  4. Value() — 实现数据共享,数据的读写是协程安全的,但如果你的数据本身进行某些操作时非协程安全,仍然是需要加锁的,例如如果使用 map 需要加锁,sync.Map 则不需要

3. Context 接口的库实现

标准库中提供了 Context 接口的几个实现。

  1. emptyCtx

  2. cancelCtx

  3. timerCtx

  4. valueCtx

4. emptyCtx

4.1. 定义

type emptyCtx int

可以看到,emptyCtx 是通过 int 别名的方式创建的,他绑定的所有上述 Context 接口中的方法都是直接返回 nil。

4.2. 创建 emptyCtx

通过 context.Background 方法就可以直接创建一个 emptyCtx。
下面是 context.Background 方法的定义:

type emptyCtx int

var (
background = new(emptyCtx)
)

func Background() Context {
return background
}

5. cancelCtx

type cancelCtx struct {
Context
mu sync.Mutex
done chan struct{}

children map[canceler]struct{}
err error
}

cancelCtx 继承了 Context 接口,同时,cancelCtx 结构内部定义了一个字段 children,是一个 canceler 类型为 key 的 map。

5.1. canceler 接口

type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}

cancel 方法传入两个参数,分别是是否从 children map 中移除该节点的 bool 类型,以及取消后,Context 的 Err() 方法将返回的 error 类型值。
一旦调用 cancel 方法,Context 的 Done 方法返回的 channel 就会立即传递一个信号,用来通知所有关注该 context 的协程执行相应的处理。

5.2. cancelCtx 的创建 — WithCancel 方法

通过 WithCancel 方法,传入 emptyCtx 就可以生成一个 cancelCtx 对象了。

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

返回的第二个参数是 CancelFunc 类型,也就是 canceler 对象中的 cancel 方法,调用即触发 context 的取消。

5.3. WithCancel 源码

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}

5.4. 实例 — 通过 cancelCtx 实现所有 goroutine 的取消

我们养了 “Alisa”, “Tom”, “Darkside”, “Robin”, “Roy” 五只狗,他们都很饿,聚在你面前要吃东西,每只狗吃一个单位的食物耗时是 100 毫秒,你现在有 100 单位的食物,如何分发这些食物,并且在分发完之后告诉所有的狗不用再继续等着食物了呢?
下面的程序展示了这个喂狗过程的模拟:

package main

import (
"context"
"fmt"
"math/rand"
"time"
)

func main() {
dogNames := []string{"Alisa", "Tom", "Darkside", "Robin", "Roy"}
ctx, cancel := context.WithCancel(context.Background())
foodChan := make(chan int, 100)
finishChan := make(chan struct{})
for _, name := range dogNames {
dog := Dog{Name:name}
go dog.dogEating(ctx, foodChan, finishChan)
}
allFoodCnt := 100
for {
count := rand.Intn(10) + 1
if allFoodCnt < count {
foodChan <- allFoodCnt
close(foodChan)
cancel()
break
} else {
allFoodCnt -= count
foodChan <- count
}
}
for i:=0; i<len(dogNames); i++ {
<-finishChan
}
close(finishChan)
}

type Dog struct {
Name string
}

func (d *Dog)dogEating(ctx context.Context, food <-chan int, finishChan chan<-struct{}) {
var allFoodCnt int
label:
for {
select {
case foodCnt := <-food:
time.Sleep(time.Duration(foodCnt * 100) * time.Microsecond)
allFoodCnt += foodCnt
fmt.Printf("dog %s eat %v\n", d.Name, foodCnt)
case <- ctx.Done():
fmt.Printf("dog %s eat %v totally\n", d.Name, allFoodCnt)
break label
}
}
finishChan <- struct{}{}
}

可以看到,我们通过一个通道 foodChan 来实现喂食,每次随机取 1-10,表示我们一把抓取的食物单位数,每次喂食一只狗,在全部食物都投喂完成后,我们通过 cancelCtx 的 cancel 方法提示所有狗这个喂食过程的结束。
最后,我们通过 finishChan 实现了 main goroutine 的等待。
执行程序,展示了:

dog Roy eat 8
dog Darkside eat 2
dog Tom eat 2
dog Robin eat 10
dog Alisa eat 8
dog Roy eat 9
dog Robin eat 7
dog Tom eat 1
dog Darkside eat 6
dog Roy eat 5
dog Robin eat 2
dog Darkside eat 10
dog Alisa eat 1
dog Tom eat 3
dog Robin eat 5
dog Robin eat 24 totally
dog Roy eat 9
dog Roy eat 31 totally
dog Alisa eat 6
dog Darkside eat 2
dog Darkside eat 20 totally
dog Alisa eat 15 totally
dog Tom eat 4
dog Tom eat 10 totally

平均每只狗都吃到了 10 到 30 单位的食物,比较符合我们的预期。

6. timerCtx

type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}

继承自 cancelCtx,增加了 timer 字段,用来实现超时机制。

6.1. 创建 timerCtx — 设置中止时间 WithDeadline

我们可以通过调用 WithDeadline 方法创建一个拥有终止时间的 timerCtx。
一旦到达定义的终止时间点,timerCtx 会自动触发取消。
与 WithCancel 一样,他除了返回 timerCtx 外,还返回一个 cancel 函数对象,在终止时间到达前,我们也可以主动调用这个 cancel 函数来取消 context。

ctx, cancel := context.WithDeadline(context.Background(), time.Parse("2006-01-02 15:04:05", "2020-08-11 11:18:46"))

6.2. 创建 timerCtx — 设置超时时间 WithTimeout

除了设置终止时间点,我们也可以通过设置超时时间来创建 timerCtx,这样,在创建 timerCtx 的指定时间后,取消会自动触发。

ctx, cancel := context.WithTimeout(context.Background(), time.Now().Add(100 * time.Second))

6.3. WithDeadline & WithTimeout 源码

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(true, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}

6.4. 实例 — 通过超时时间中止所有 goroutine

如果每只狗吃饭的速度都太慢,超过了我们等待的限度呢?
timerCtx 提供了超时设置,利用 timerCtx 我们就可以方便的实现这个功能了:

package main

import (
"context"
"fmt"
"math/rand"
"time"
)

func main() {
dogNames := []string{"Alisa", "Tom", "Darkside", "Robin", "Roy"}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
foodChan := make(chan int, 100)
finishChan := make(chan struct{})
for _, name := range dogNames {
dog := Dog{Name:name}
go dog.dogEating(ctx, foodChan, finishChan)
}
allFoodCnt := 100
for {
count := rand.Intn(10) + 1
if allFoodCnt < count {
foodChan <- allFoodCnt
close(foodChan)
break
} else {
allFoodCnt -= count
foodChan <- count
}
}
for i:=0; i<len(dogNames); i++ {
<-finishChan
}
close(finishChan)
}

type Dog struct {
Name string
}

func (d *Dog)dogEating(ctx context.Context, food <-chan int, finishChan chan<-struct{}) {
var allFoodCnt int
for {
select {
case foodCnt := <-food:
time.Sleep(time.Duration(foodCnt * 1) * time.Second)
allFoodCnt += foodCnt
fmt.Printf("dog %s eat %v\n", d.Name, foodCnt)
case <- ctx.Done():
fmt.Printf("dog %s eat %v totally\n", d.Name, allFoodCnt)
finishChan <- struct{}{}
return
}
}
}

执行打印出了:

dog Roy eat 2
dog Robin eat 2
dog Alisa eat 8
dog Alisa eat 8 totally
dog Tom eat 8
dog Robin eat 6
dog Tom eat 1
dog Tom eat 9 totally
dog Darkside eat 10
dog Darkside eat 1
dog Roy eat 9
dog Roy eat 2
dog Robin eat 7
dog Robin eat 15 totally
dog Darkside eat 5
dog Darkside eat 16 totally
dog Roy eat 3
dog Roy eat 10
dog Roy eat 26 totally

可以看到,五只狗并没有吃完全部的 100 单位食物,因为超时时间,他们提前终止了吃食的过程。

7. valueCtx

type valueCtx struct {
Context
key, val interface{}
}

继承自 Context,实现了用来存储数据的 key、val 键值对的 context。
这可以说是最为常用的一种 context 了,在全局传递一些参数,例如 trace_id,来贯穿整个调用链是最为常见的做法。

7.1. 创建 valueCtx 并携带信息

通过 WithValue 方法,我们可以实现带有数据的 context — valueCtx 的创建。

ctx := context.WithValue(context.Background(), "trace_id", "1387211")
ctx = context.WithValue(ctx, "session", 1)

我们创建了带有数据 {trace_id: 1387211, session: 1} 的 valueCtx。
此后,我们通过 ctx.Value() 方法就可以取出数据:

traceID := ctx.Value("trace_id").(string)

7.2. WithValue 源码

func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflect.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}

7.3. 示例 — goroutine 参数传递

package main

import (
"context"
"fmt"
"github.com/google/uuid"
)

func main() {
traceId := uuid.New()
userId := 1386
ctx := context.WithValue(context.Background(), "trace_id", traceId)
ctx = context.WithValue(ctx, "user_id", userId)
userInfo := getUserInfo(ctx)
fmt.Printf("[main] (trace_id: %v, user_id: %v) after getUserInfo %v",
ctx.Value("trace_id").(int), ctx.Value("user_id").(int), userInfo)
}

type UserInfo struct {
UserId int
UserName string
}

func getUserInfo(ctx context.Context) UserInfo {
fmt.Printf("[getUserInfo] (trace_id: %v, user_id: %v) get user info from db",
ctx.Value("trace_id").(int), ctx.Value("user_id").(int))
return UserInfo{UserId: ctx.Value("user_id").(int), UserName: "Tome"}
}

8. 微信公众号

欢迎关注微信公众号,以技术为主,涉及历史、人文等多领域的学习与感悟,每周三到七篇推文,只有全部原创,只有干货没有鸡汤。

阅读原文

通过上下文协作 — 详解 golang 中的 context》来自互联网,仅为收藏学习,如侵权请联系删除。本文URL:https://www.bookhoes.com/5269.html