通过上下文协作 — 详解 golang 中的 context
1. 引言
通过 golang 中的 goroutine 实现并发编程是十分简单方便的,此前我们进行了非常详细的介绍,并且看到了如何通过 channel 来协调多个并发的 goroutine。
GoLang 的并发编程与通信 — goroutine 与通道
channel 本质上是用来在多个 goroutine 之间进行数据传输的通道,用它来进行 goroutine 并发的协调看起来有些繁琐。
golang 1.7 引入了 Context,可以十分方便的实现:
-
多个 goroutine 之间数据共享和交互
-
goroutine 超时控制
-
运行上下文控制
本文我们就来详细介绍一下 golang 中的 context 的使用。
2. Context 接口
golang 中 Context 本质上是一个接口:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
他声明了四个方法:
-
Deadline() — 返回 bool 类型的 ok 表示是否定义了超时时间,time.Time 类型的 deadline 则表示 context 应该结束的时间
-
Done() — 返回一个 channel,当到达 deadline 所定义的时间或 context 被取消时,返回的 channel 会传递一个信号
-
Err() — 返回 context 被取消的原因
-
Value() — 实现数据共享,数据的读写是协程安全的,但如果你的数据本身进行某些操作时非协程安全,仍然是需要加锁的,例如如果使用 map 需要加锁,sync.Map 则不需要
3. Context 接口的库实现
标准库中提供了 Context 接口的几个实现。
-
emptyCtx
-
cancelCtx
-
timerCtx
-
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