通过 golang 中的 goroutine 实现并发编程是十分简单方便的,此前我们进行了非常详细的介绍,并且看到了如何通过 channel 来协调多个并发的 goroutine。 GoLang 的并发编程与通信 — goroutine 与通道
channel 本质上是用来在多个 goroutine 之间进行数据传输的通道,用它来进行 goroutine 并发的协调看起来有些繁琐。 golang 1.7 引入了 Context,可以十分方便的实现:
本文我们就来详细介绍一下 golang 中的 context 的使用。
golang 中 Context 本质上是一个接口:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
他声明了四个方法:
标准库中提供了 Context 接口的几个实现。
type emptyCtx int
可以看到,emptyCtx 是通过 int 别名的方式创建的,他绑定的所有上述 Context 接口中的方法都是直接返回 nil。
通过 context.Background 方法就可以直接创建一个 emptyCtx。 下面是 context.Background 方法的定义:
type emptyCtx int
var (
background = new(emptyCtx)
)
func Background() Context {
return background
}
type cancelCtx struct {
Context
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}
cancelCtx 继承了 Context 接口,同时,cancelCtx 结构内部定义了一个字段 children,是一个 canceler 类型为 key 的 map。
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
cancel 方法传入两个参数,分别是是否从 children map 中移除该节点的 bool 类型,以及取消后,Context 的 Err() 方法将返回的 error 类型值。 一旦调用 cancel 方法,Context 的 Done 方法返回的 channel 就会立即传递一个信号,用来通知所有关注该 context 的协程执行相应的处理。
通过 WithCancel 方法,传入 emptyCtx 就可以生成一个 cancelCtx 对象了。
ctx, cancel := context.WithCancel(context.Background())
返回的第二个参数是 CancelFunc 类型,也就是 canceler 对象中的 cancel 方法,调用即触发 context 的取消。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
我们养了 "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 单位的食物,比较符合我们的预期。
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
继承自 cancelCtx,增加了 timer 字段,用来实现超时机制。
我们可以通过调用 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"))
除了设置终止时间点,我们也可以通过设置超时时间来创建 timerCtx,这样,在创建 timerCtx 的指定时间后,取消会自动触发。
ctx, cancel := context.WithTimeout(context.Background(), time.Now().Add(100 * time.Second))
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))
}
如果每只狗吃饭的速度都太慢,超过了我们等待的限度呢? 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 单位食物,因为超时时间,他们提前终止了吃食的过程。
type valueCtx struct {
Context
key, val interface{}
}
继承自 Context,实现了用来存储数据的 key、val 键值对的 context。 这可以说是最为常用的一种 context 了,在全局传递一些参数,例如 trace_id,来贯穿整个调用链是最为常见的做法。
通过 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)
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}
}
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"}
}