Context详解

Context详解

同步和异步机制

同步和异步机制关注的是通信机制

同步:调用端需要等待被调用端返回信息才继续运行

异步:不用理会被调用端,一直执行

很显然,Go中的goroutine通信机制就是一种异步机制

goroutine之间的通信

因为goroutine一开始就不受控制了,所以我们必须要想到一种优雅的控制goroutine的方式。

全局变量

这是一种最简单最暴力的控制goroutine的方法,我们不断对一个全局变量判断,当满足某某条件时,让全局变量变为false或true,从而对goroutine进行控制

sync.WaitGroup

先介绍一下里面最重要的几个函数

 var wg sync.WaitGroup
 wg.Add(n) //增加n个进程
 wg.Wait()   //等待所有进程结束
 wg.Done()   //表示此进程结束,Wait需要等待的进程数减一

一个栗子

 func main()  {
    t:=time.Now()
    var wg sync.WaitGroup
    wg.Add(2)
    go func() {
      fmt.Println("小黄好好看")
      wg.Done()
    }()
    go func() {
          fmt.Println("小程也好看")
      time.Sleep(1*time.Second)
      wg.Done()
    }()
    wg.Wait()
    fmt.Println("done!")
    t1:=time.Since(t)
    fmt.Println("cost time:",t1)
 } //通过sync.WaitGroup控制并发

我们知道,通过sync.WaitGroup控制并发是一种非常传统的方式了,想一想就能发现,当有很多goroutine以及goroutine嵌套的时候,sync包控制并发是非常繁琐的一件事情。

chan+select

这是一种安全且占用资源少的goroutine退出方式

栗子:

 func main()  {
    stop:=make(chan bool)
    go func() {
      for {
          select {
          case <- stop:
            fmt.Println("监控退出,停止...")
            return
          default:
            fmt.Println("goroutine监控中")
            time.Sleep(1*time.Second)
          }
      }
    }()
    time.Sleep(5*time.Second)
    fmt.Println("使用chan通知停止监控")
    stop<-true
 }

因为chan是并发安全的 所以我们使用chan通知goroutine退出是一种比较优雅、安全的方式 但是还是能看出来:一旦goroutine数量过多 或者因为goroutine自身的链式关系 比如嵌套 衍生等等 那么chan还是很难成功处理这种场景的

Context

上面提到的嵌套goroutine和衍生等情况是存在的,比如一个网络API接口需要一个goroutine处理部分需求,这个goroutine可能又会开启其他的goroutine 所以我们需要一种可以跟踪goroutine的方案 才能够达到控制它们的目的 这就是Go官方指定的处理并发编程的工具:Context 中文翻译为上下文 。

Context在通知多个goroutine或者嵌套等待goroutine退出的时候是非常方便的,于是在go1.7版本之后,官方发行了context包,用于管理协程的退出

Context接口

 type Context interface{
  Deadline() (deadline time.Time,ok bool) //返回当前Context被取消的时间,也就是完成工作的截止时间和是否完成任务
   
 Done()             <-chan struct{}           //Done方法需要返回一个只读的Channel 这个Channel会在当前工作完成或者上下文被取消之后关闭 多次调用Done方法会返回同一个Channel
 ​
 Err()     error     //被取消返回Canceled错误 超时返回DeadlineExceeded错误
 ​
 Value(key interface{}) interface{}              
    //给Context带上一个键值对
 }

context控制一个goroutine:

 //使用context重写上文
 package main
 ​
 ​
 import (
    "context"
    "fmt"
    "time"
 )
 ​
 ​
 func main() {
    ctx,cancel:=context.WithCancel(context.Background())
    go func(ctx context.Context) {
          for {
            select {
            case <-ctx.Done():
              fmt.Println("监控退出,停止...")
              return
            default:
              fmt.Println("goroutine监控中")
              time.Sleep(1*time.Second)
          }
        }
    }(ctx)
    time.Sleep(5*time.Second)
    fmt.Println("使用context通知停止监控")
    cancel()
   
 }
 context.Background() 返回一个空的context 这个空的context一般作用于整个Context树的根节点

Context的一些函数

 context.Background() //返回一个空的Context 这个Context可以作为所有context的根节点
 ​
 context.Todo() //目前没有具体的使用场景,当我们不知道该使用什么Context的时候,可以使用这个
 ​
 上面两个方法实现了Context的所有接口 它们的本质是
 emptyCtx结构体类型 不可取消 没有截止时间 也没有value
 ​
 context.WithCancel(parent) //创建了一个可取消的子Context 创建一个可取消的子Context 然后我们把这个子Context当作参数传给goroutine,这样就可以使用这个子Context跟踪这个goroutine 然后再在goroutine中使用select <-ctx.Done()
 判断是否要结束 如果收到了这个值 就返回结束goroutine,如果接收不到,那么就继续进行监控
 ​
 cancel() //发送结束命令 这个函数就是WithCancel的第二个返回值

context控制多个goroutine:

 func main()  {
    ctx,cannel:=context.WithCancel(context.Background())
    go watch(ctx,"监控1")
    go watch(ctx,"监控2")
    go watch(ctx,"监控3")
    time.Sleep(5*time.Second)
    fmt.Println("使用context通知多个goroutine退出")
    cannel()
 }
 func watch(ctx context.Context,name string) {
    for {
      select {
          case <-ctx.Done():
              fmt.Println(name,"监控退出,停止了...")
            return
      default:
          fmt.Println(name,"goroutine监控中...")
          time.Sleep(1*time.Second)
      }
    }
 } //使用Context通知多个goroutine退出

每个goroutine传入的context都是context.Background()生成的同一个子context 当我们使用cancel函数通知退出 三个goroutine都i会收到消息 这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个 Context和衍生的子Context只要是被当作参数传给了goroutine,都可以控制goroutine的退出,不得不说这是一种优雅的处理goroutine启动之后不可控的方法

Context的衍生继承

有了根Context如何衍生出更多的子Context呢?这就需要用到我们的With系列函数了

 func WithCancel(parent Context) (ctx Context,cancel CancelFunc)
 ​
 func WithDeadline(parent Context,deadline time.Time) (Context,CancelFunc)
 ​
 func WitchTimeout(parent Context,timeout time.Time) (Context,CancelFunc)
 ​
 func WithValue(parent Context,key,val interface{}) Context

这四个with函数接受的都是父亲Context 并且生成子Context 前三个返回值里面都有CanceFunc(取消函数),它就像一个控制器一样,一旦触发,无论子Context有多少个,叠加多少层都会一次性关闭。第二个With函数和第三个With函数都是设立一个过期时间,非常类似第四个函数是给Context绑定一个键值对

Context使用原则

  • 不要把Context放在结构体里面,而是要当作参数传递

  • 以Context作为参数的函数方法,应该把Context当作第一个参数

  • 给一个函数方法传递Context的时候,如果不知道具体应该传什么,那么不要传nil,传context.Todo

  • context的value相关方法应该传递必须的函数,不要什么数据都用value尽心传递

  • Context是线程安全的,可以放心的在多个goroutine中传递

上一篇:sync mutex golang


下一篇:极客大学Go 进阶训练营