Go语言高并发系列三:context

947 阅读4分钟

上一篇文章《Go语言高并发系列二:Go语言并发基础》介绍了channel、sync包,用于并发控制。

这篇文章我们来学习一下context。

context是什么

Go 1.7标准库引入了context。 context可以用来在goroutine之间传递上下文信息,包含:取消信号,超时信号,截止信号,k-v数据等。

相同的context可以传递给多个goroutine,context是goroutine安全的。

backgroundtodo可以创建一个context
WithDeadlineWithTimeoutWithCancel 、 WithValue 可以创建一个子context

现在context基本上已经作为goroutine并发控制和超时控制的标准做法了。

看到这里是不是两脸懵逼?没关系,往下看,后面慢慢介绍。

context有什么用

Go一般用来做后端服务,比如我们搭建一个http server。

http server会接收到很多来自客户端的请求。一般来说接收到一个请求时,会有若干个goroutine同时工作,有的去数据库取数据,有的去调用下游的接口...
这些goroutine关联起来可以形成一个树状图:

image.png

这些goroutine一般会共享一些数据,比如登录token等。如果请求被中断,比如浏览器关闭、执行超时等,这个请求相关的goroutine应该被及时关闭。因为它们工作成果已经不再被需要了,当这些goroutine退出后,系统就可以回收资源。

如果goroutine没有被及时关闭会怎么样?

假设请求内的某个goroutine因为某些原因,执行变得非常慢。当请求超时以后,可能用户收到网关请求超时的错误,但此时服务内部goroutine没有被及时关闭,它仍然会在后台默默的执行。
持续收到请求,goroutine的数量就会持续增长,直到系统资源被用完,甚至发生服务器宕机,服务不可用的情况。

在Go里面,外部不能直接关闭goroutine,需要由goroutine自己退出。在简单情况下,可以用channel+select的方式来控制协程关闭。但是在复杂场景下,用channel+select的方式就会很麻烦,也容易产生bug。用context来控制就比较方便。

context树

前面我们说了,可以把一个请求内创建的若干个goroutine看做一颗树。
现在我们给这棵树的每个节点都带上一个context,可以得到一个context树。
这个context树中的节点,可以是父context的副本,也可以是共用相同的context。

image.png 左边是goroutine树,右边是一一对应的context树

当某个context超时,或者被cancel的话。它相关联的子context也会被同时取消。

image.png 如上左图标红的context触发cancel以后,它会从它的父context去除掉,同时它的子context都会被cancel。看右图,圈出来的部分,都被cancel,且父子关系不存在了。

怎么操作context

有两种方式可以创建context:

  • context.Background 是context的默认值,其他的context都应该从它衍生出来
  • context.Todo 是在不确定用哪个context时使用

这两种方式得到的结果都是一样的,只是互为别名而已,得到的都是根context。

超时控制

  • WithTimeout 接收持续时间作为参数,返回一个携带持续时间的子context和CancelFunc
  • WithDeadline 接收到期时间作为参数,返回一个携带到期时间的子context和CancelFunc

子context在到期后会自动取消context。也可以通过执行CancelFunc手动取消context。

WithCancel

创建一个子context并返回一个cancel函数,子context没有到期时间。执行cancel函数可以手动取消context。

WithValue

WithValue接收 k,v 两个参数,创建一个新的context并在context上保存k,v。新的context会指向它的父context。如果执行多次WithValue,就会得到一串像链表一样的context。

image.png 当从某个context取值的时候,它会顺着链路一直往上查找,直到查找到对应的k。

实例

创建3个goroutine,并创建3个context控制goroutine依次结束

func deal(ctx context.Context, name string) {
   go func() {
      for true {
         select {
         case <-ctx.Done():
            fmt.Println("结束:", name)
            return
         default:
            <-time.After(100 * time.Millisecond)
         }
      }
   }()
}

func main() {

   bg := context.Background()
   ctx1, _ := context.WithTimeout(bg, time.Second)
   deal(ctx1, "WithTimeout")

   ctx2, _ := context.WithDeadline(bg, time.Now().Add(2*time.Second))
   deal(ctx2, "WithDeadline")

   ctx3, cancel := context.WithCancel(bg)
   deal(ctx3, "WithCancel")

   <-time.After(3 * time.Second)
   cancel()

   <-time.After(time.Second)

}

输出:

结束: WithTimeout
结束: WithDeadline
结束: WithCancel

总结

Context为我们主要定义四种方WithDeadline,WithTimeout,WithValue,WithCancel,从而达到控制goroutine的目的。
使用Context可以高效简洁的达到控制goroutine的目的。

下一篇文章我们来探讨一些常见的并发模式。
并发模式,顾名思义,就是goroutine并发的一些使用方式。