1. 什么是 Context

一个任务会有很多个 goroutine 协作完成,一次 HTTP 请求也会触发很多个 goroutine 的启动,而这些 goroutine 有可能会启动更多的子 goroutine ,并且无法预知有多少层 goroutine 、每一层有多少个 goroutine

如果因为某些原因导致任务终止了,HTTP 请求取消了,那么它们启动的 goroutine 怎么办?该如何取消呢?因为取消这些 goroutine 可以节约内存,提升性能,同时避免不可预料的 Bug。

Context 就是用来简化解决这些问题的,并且是并发安全的。Context 是一个接口,它具备手动、定时、超时发出取消信号、传值等功能,主要用于控制多个 goroutine 之间的协作,尤其是取消操作。一旦取消指令下达,那么被 Context 跟踪的这些 goroutine 都会收到取消信号,就可以做清理和退出操作。

Goroutine 的应用更多的需要配合 context 来做并发任务的处理:

  • 传递数据
  • 主动取消
  • 超时取消

2. Context 接口方法

Context 接口只有四个方法:

type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key interface{}) interface{}
}
  • Deadline 方法可以获取设置的截止时间,第一个返回值 deadline 是截止时间,到了这个时间点,Context 会自动发起取消请求,第二个返回值 ok 代表是否设置了截止时间。

  • Done 方法返回一个只读的 channel,类型为 struct{}。在 goroutine 中,如果该方法返回的 chan 可以读取,则意味着 Context 已经发起了取消信号。通过 Done 方法收到这个信号后,就可以做清理操作,然后退出 goroutine ,释放资源。

  • Err 方法返回取消的错误原因,即因为什么原因 Context 被取消。

  • Value 方法获取该 Context 上绑定的值,是一个键值对,所以要通过一个 key 才可以获取对应的值。

Context 接口的四个方法中最常用的就是 Done 方法,它返回一个只读的 channel,用于接收取消信号。当 Context 取消的时候,会关闭这个只读 channel,也就等于发出了取消信号。

3. Context 树

我们不需要自己实现 Context 接口,Go 语言提供了函数可以帮助我们生成不同的 Context,通过这些函数可以生成一颗 Context 树,这样 Context 才可以关联起来,父 Context 发出取消信号的时候,子 Context 也会发出,这样就可以控制不同层级的 goroutine 退出。

从使用功能上分,有四种实现好的 Context

  • 空 Context:不可取消,没有截止时间,主要用于 Context 树的根节点。

  • 可取消的 Context:用于发出取消信号,当取消的时候,它的子 Context 也会取消。

  • 可定时取消的 Context:多了一个定时的功能。

  • 值 Context:用于存储一个 key-value 键值对。

Go 语言中,可以通过 context.Background() 获取一个根节点 Context

有了根节点 Context 后,这颗 Context 树要怎么生成呢?需要使用 Go 语言提供的四个函数。

  • WithCancel(parent Context) (ctx Context, cancel CancelFunc):生成一个可取消的 Context。

  • WithDeadline(parent Context, d time.Time) (Context, CancelFunc):生成一个可定时取消的 Context,参数 d 为定时取消的具体时间。

  • WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc):生成一个可超时取消的 Context,参数 timeout 用于设置多久后取消

  • WithValue(parent Context, key, val interface{}) Context:生成一个可携带 key-value 键值对的 Context。

以上四个生成 Context 的函数中,前三个都属于可取消的 Context,它们是一类函数,最后一个是值 Context,用于存储一个 key-value 键值对。

3.1 context.WithCancel 取消多个 goroutine

package mainimport ("context""fmt""sync""time"
)func main() {var wg sync.WaitGroupwg.Add(3)ctx, stop := context.WithCancel(context.Background())go func() {defer wg.Done()watchDog(ctx, "watchdog_1")}()go func() {defer wg.Done()watchDog(ctx, "watchdog_2")}()go func() {defer wg.Done()watchDog(ctx, "watchdog_3")}()time.Sleep(5 * time.Second)stop() //发停止指令wg.Wait()
}func watchDog(ctx context.Context, name string) {//开启for select循环,一直后台监控for {select {case <-ctx.Done():fmt.Println(name, "receive stop cmd, will stop")returndefault:fmt.Println(name, "is running ……")}time.Sleep(1 * time.Second)}
}

使用 context.WithCancel(context.Background()) 函数生成一个可以取消的 Context,用于发送停止指令。这里的 context.Background() 用于生成一个空 Context,一般作为整个 Context 树的根节点。

3.2 context.WithValue 传值

Context 不仅可以取消,还可以传值,通过这个能力,可以把 Context 存储的值供其他 goroutine 使用。我通过下面的代码来说明:

package mainimport ("context""fmt""sync""time"
)func main() {var wg sync.WaitGroupwg.Add(4)ctx, stop := context.WithCancel(context.Background())go func() {defer wg.Done()watchDog(ctx, "watchdog_1")}()go func() {defer wg.Done()watchDog(ctx, "watchdog_2")}()go func() {defer wg.Done()watchDog(ctx, "watchdog_3")}()valCtx := context.WithValue(ctx, "userId", 2)go func() {defer wg.Done()getUser(valCtx)}()time.Sleep(5 * time.Second)stop() //发停止指令wg.Wait()
}func watchDog(ctx context.Context, name string) {//开启for select循环,一直后台监控for {select {case <-ctx.Done():fmt.Println(name, "receive stop cmd, will stop")returndefault:fmt.Println(name, "is running ……")}time.Sleep(1 * time.Second)}
}func getUser(ctx context.Context) {for {select {case <-ctx.Done():fmt.Println("user exit")returndefault:userId := ctx.Value("userId")fmt.Println("userId is", userId)time.Sleep(1 * time.Second)}}
}

其中,通过 context.WithValue 函数存储一个 userId 为 2 的键值对,就可以在 getUser 函数中通过 ctx.Value("userId") 方法把对应的值取出来,达到传值的目的。

其它示例:
实际开发过程中,为了能够保持链路追踪(traceID),需要把每个核心函数中都串联起来,这样就需要通过 context.WithValue来传值,把相关数据传递下去。另外一个场景就是一些核心元数据,需要一直传递给相关方法,但是不想暴露参数,那么也可以通过 context.WithValue 来传值,示例如下:

package mainimport ("fmt""time""golang.org/x/net/context"
)var key = "traceId"func startTask(ctx context.Context) {for {select {case <-ctx.Done()://get valuefmt.Println(ctx.Value(key), "is cancel")returndefault://get valuefmt.Println(ctx.Value(key), "int goroutine")time.Sleep(1 * time.Second)}}
}func main() {ctx, cancel := context.WithCancel(context.Background())valueCtx := context.WithValue(ctx, key, "allen.wu")go startTask(valueCtx)time.Sleep(5 * time.Second)cancel()time.Sleep(3 * time.Second)
}

3.3 context.WithTimeout 超时取消

在实际开发过程,当我们需要开一个 Goroutine 来做一些耗时的操作的时候,我们可能需要控制超时时间,超时后,让 Goroutine 退出。

比如,在我实际开发 IM 消息系统的架构中,我需要从 DB 中拉取消息,因此我要控制好超时,怎么控制呢? 通过 context.WithTimeout 设置超时时间,然后把返回的新的上下文传递到 Goroutine 任务中,这样,超时后自动退出并清理,示例如下:

package mainimport ("fmt""sync""time""golang.org/x/net/context"
)var (wg sync.WaitGroup
)func startTask(ctx context.Context) error {defer wg.Done()for i := 0; i < 30; i++ {select {case <-time.After(2 * time.Second):fmt.Printf("in goroutine do task %v\n", i)// we received the signal of cancelation in this channelcase <-ctx.Done():fmt.Printf("cancel goroutine task %v\n", i)return ctx.Err()}}return nil
}func main() {timeoutCtx, cancel := context.WithTimeout(context.Background(), 4*time.Second)defer cancel()fmt.Println("startTask")wg.Add(1)go startTask(timeoutCtx)wg.Wait()fmt.Println("endTask")
}

3.4 调用 Context cancel 和 Done 取消任务

并发编程中,当前 go 一个任务来并发处理之后,我们怎么控制让这个任务在合适的时机能够退出呢?其中一个姿势,就是通过 context.WithCancel 生成一个新的上下文,然后将这个新的上下文传递到 goroutine 中,然后监听 context 事件,如果有退出事件发生,则退出 goroutine 任务:

package mainimport ("fmt""time""golang.org/x/net/context"
)func startTask(ctx context.Context, task string) {for {select {case <-ctx.Done():fmt.Println("stop goroutine startTask")returndefault:fmt.Println(task, "in goroutine do task")time.Sleep(2 * time.Second)}}
}func main() {ctx, cancel := context.WithCancel(context.Background())go startTask(ctx, "start allen.wu task 1")go startTask(ctx, "start allen.wu task 2")time.Sleep(6 * time.Second)fmt.Println("Now, call func cancel to stop all goroutines")cancel()time.Sleep(5 * time.Second)
}

6. Context 使用原则

Context 是一种非常好的工具,使用它可以很方便地控制取消多个 goroutine 。在 Go 语言标准库中也使用了它们,比如 net/http 中使用 Context 取消网络的请求。

要更好地使用 Context,有一些使用原则需要尽可能地遵守。

  • Context 不要放在结构体中,要以参数的方式传递。

  • Context 作为函数的参数时,要放在第一位,也就是第一个参数。

  • 要使用 context.Background 函数生成根节点的 Context,也就是最顶层的 Context

  • Context 传值要传递必须的值,而且要尽可能地少,不要什么都传。

  • Contextgoroutine 安全,可以在多个 goroutine 中放心使用。

  • 可以把一个 Context 对象传递给任意个数的 Gorotuine,对它执行 取消 操作时,所有 goroutine 都会接收到取消信号。

  • Context 一般是作为函数的参数进行传递,并且最优的做法是把 Context 作为第一个参数放到每个关键函数的参数中,并且变量名都建议统一命名,名为 ctx

  • 一般而言,把 context.Background() 作为第一个 parent Context

  • ContextValue 中应该传递必须的核心元数据,不要什么数据都使用 Context 传递。

  • 永远记住,只要传递 Context,就不要把 Context 设置为 nil 来传递。

以上原则是规范类的,Go 语言的编译器并不会做这些检查,要靠自己遵守。

7. 总结

Context 通过 With 系列函数生成 Context 树,把相关的 Context 关联起来,这样就可以统一进行控制。一声令下,关联的 Context 都会发出取消信号,使用这些 Contextgoroutine 就可以收到取消信号,然后清理退出。

在定义函数的时候,如果想让外部给你的函数发取消信号,就可以为这个函数增加一个 Context 参数,让外部的调用者可以通过 Context 进行控制,比如下载一个文件超时退出的需求。

其它文章:
Golang 之 Context 的迷思

Go 学习笔记(68)— goroutine 并发控制神器 Context相关推荐

  1. Python自然语言处理学习笔记(68):7.9 练习

    7.9   Exercises  练习 ☼ The IOB format categorizes tagged tokens as I, O and B. Why are three tags nec ...

  2. IOS学习笔记68 -- 开发的一些奇巧淫技

    TableView不显示没内容的Cell怎么办? 类似这种,我不想让下面那些空的显示. 很简单. 1 self.tableView.tableFooterView = [[UIView alloc]  ...

  3. 学习笔记68—PhotoShop常见快捷键

    1.快捷键:Ctrl+D----->取消选区 转载于:https://www.cnblogs.com/hechangchun/p/10583998.html

  4. Golang中WaitGroup、Context、goroutine定时器及超时学习笔记

    原文连接:http://targetliu.com/2017/5/2... 好久没有发过文章了 - -||,今天发一篇 golang 中 goroutine 相关的学习笔记吧,以示例为主. WaitG ...

  5. 【代码质量】-阿里巴巴java开发手册(代码质量提升神器)学习笔记

    前言:<阿里巴巴 Java 开发手册>是阿里巴巴集团技术团队的集体智慧结晶和经验总结,有了这些前人总结的经验,可以帮助我们写出高质量的代码,同时可以减少Bug数量,少踩坑,提高代码的可读性 ...

  6. catBoost 神器的学习笔记

    catBoost 神器的学习笔记,记录自己看原文章的心得.第一次发文,中间有些部分也是个人理解,不足之处,敬请谅解.欢迎扔砖 ^=^catBoost 原文的标题是 "CatBoost :un ...

  7. Go学习笔记—标准库Context

    标准库Context ​ 由于goroutine没有父子关系,多个goroutine都是被平行的调度,所以在拉起多个goroutine后,程序的执行模型并没有维护树状结构的goroutine树,所以无 ...

  8. 十分钟了解绘图神器——Graphviz学习笔记

    DOT语言学习笔记 设置点和线的形状与颜色 digraph是有向图,graph是无向图.要注意,->和–都表示图中的一条边,但是前者用于有向图中,而后者用于无向图中,不能混用. 代码示例 dia ...

  9. Go语学习笔记 - goroutine | 从零开始Go语言

    学习笔记,写到哪是哪. 关于golang的协程早有耳闻,具体概念不过多赘述了,可以理解为轻量级的线程,一个线程中可以存在多个协程.协程在同一个线程中,只有一个协程在运行.协程调度切换时,将寄存器上下文 ...

最新文章

  1. timesten 修改最大连接数
  2. sm总线控制器找不到驱动程序_技术 | 基于CAN总线的伺服电机通信控制
  3. 关于JDBC的一些笔记
  4. 面试时如何优雅地自我介绍?
  5. batik-all-1.7
  6. 从汇编的眼光看C++(之泛型编程)
  7. 移动网络新漏洞影响2G 以来的所有蜂窝网络
  8. spring事务源码-代理对象生成过程解析
  9. Python+Pid实现车辆速度跟踪
  10. 《麦肯锡方法》读书笔记4
  11. 面试必备 | 机器学习这十大算法你确定会了吗?
  12. 利用python进行TEQC质量检核结果绘图
  13. 投资学U14 债券的估值和收益率 习题解读
  14. linux下 Apache 配置虚拟主机三种方式
  15. 校招总结--建议全文背诵
  16. catia刨面命令_Catia查看装配体剖面的操作方法
  17. 几行代码解决京东购物界面
  18. 《用C#制作PDF文件全攻略》
  19. FAQ 检索式问答系统学习记录
  20. matlab并联lc谐振电路图,串联谐振和并联谐振LC电路操作

热门文章

  1. 2022-2028年中国产后修复行业市场研究及前瞻分析报告
  2. 桌子上有个盘子_日本留学生活:留学生在餐厅刷盘子的传闻,竟然在自己身上上演...
  3. Go 知识点(08) — 对未初始化的 channel 进行读写操作
  4. 【VS实践】如何在vs中自动添加注释
  5. 面向过程(或者叫结构化)分析方法与面向对象分析方法到底区别在哪里?
  6. 矩阵的卷积核运算(一个简单小例子的讲解)深度学习
  7. 【Tensorflow】tf.nn.atrous_conv2d如何实现空洞卷积?膨胀卷积
  8. LeetCode简单题之数组元素积的符号
  9. MySQL最新版8.0.21安装配置教程~
  10. TVM apps extension示例扩展库