前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过上下文协作 -- 详解 golang 中的 context

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

作者头像
用户3147702
发布2022-06-27 14:18:10
2950
发布2022-06-27 14:18:10
举报
文章被收录于专栏:小脑斧科技博客

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 本质上是一个接口:

代码语言:javascript
复制
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. 定义

代码语言:javascript
复制
type emptyCtx int

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

4.2. 创建 emptyCtx

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

代码语言:javascript
复制
type emptyCtx int

var (
    background = new(emptyCtx)
)

func Background() Context {
    return background
}

5. cancelCtx

代码语言:javascript
复制
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 接口

代码语言:javascript
复制
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 对象了。

代码语言:javascript
复制
ctx, cancel := context.WithCancel(context.Background())

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

5.3. WithCancel 源码

代码语言:javascript
复制
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 单位的食物,如何分发这些食物,并且在分发完之后告诉所有的狗不用再继续等着食物了呢? 下面的程序展示了这个喂狗过程的模拟:

代码语言:javascript
复制
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

代码语言:javascript
复制
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。

代码语言:javascript
复制
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 的指定时间后,取消会自动触发。

代码语言:javascript
复制
ctx, cancel := context.WithTimeout(context.Background(), time.Now().Add(100 * time.Second))

6.3. WithDeadline & WithTimeout 源码

代码语言:javascript
复制
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 我们就可以方便的实现这个功能了:

代码语言:javascript
复制
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

代码语言:javascript
复制
type valueCtx struct {
    Context
    key, val interface{}
}

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

7.1. 创建 valueCtx 并携带信息

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

代码语言:javascript
复制
ctx := context.WithValue(context.Background(), "trace_id", "1387211")
ctx = context.WithValue(ctx, "session", 1)

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

代码语言:javascript
复制
traceID := ctx.Value("trace_id").(string)

7.2. WithValue 源码

代码语言:javascript
复制
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 参数传递

代码语言:javascript
复制
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"}
}
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-01-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小脑斧科技博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 引言
  • 2. Context 接口
  • 3. Context 接口的库实现
  • 4. emptyCtx
    • 4.1. 定义
      • 4.2. 创建 emptyCtx
      • 5. cancelCtx
        • 5.1. canceler 接口
          • 5.2. cancelCtx 的创建 — WithCancel 方法
            • 5.3. WithCancel 源码
              • 5.4. 实例 — 通过 cancelCtx 实现所有 goroutine 的取消
              • 6. timerCtx
                • 6.1. 创建 timerCtx — 设置中止时间 WithDeadline
                  • 6.2. 创建 timerCtx — 设置超时时间 WithTimeout
                    • 6.3. WithDeadline & WithTimeout 源码
                      • 6.4. 实例 — 通过超时时间中止所有 goroutine
                      • 7. valueCtx
                        • 7.1. 创建 valueCtx 并携带信息
                          • 7.2. WithValue 源码
                            • 7.3. 示例 — goroutine 参数传递
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档