• Go Context包


    前言

            context是Go语言1.7版本加入官方库,官方常用于处理单个请求的多个goroutine与请求域的数据、截止时间、同步信号和传递请求值等相关的操作。学习context,有利于大家更好的使用goroutine,提升对并发编程的理解。

    一、为什么需要context

            在典型的http服务中,每一个http请求的request都会启动一个goroutine处理请求,此请求后续还可能操作数据库、缓存、日志等,此时由最早的goroutine启动后续的多个goroutine,这样就使用多个goroutine处理一个request请求,而context就是在几个不同goroutine直接同步数据、取消信号以及处理请求截至日期。

            context最常规的做法就是从goroutine开始,一层层地把信息传递到最下层。如果没有context,就可能发生上层的已经因为报错而结束,但是下层的goroutine却还在继续。

            如果有了context,当上层goroutine发生错误而结束时,可以很快地同步信息到其下层的goroutine,这样可以及时停止下层goroutine,避免无谓的系统消耗。

    二、context

    1.context接口

            context包的核心接口,其定义如下

    1. // 上下文携带截止日期、取消信号以及跨 API 边界的其他值。
    2. // Context 的方法可以被多个 goroutine 同时调用。
    3. type Context interface {
    4. Deadline() (deadline time.Time, ok bool)
    5. Done() <-chan struct{}
    6. Err() error
    7. Value(key interface{}) interface{}
    8. }

            Context接口内定义了四个方法,分别如下。

    • Deadline():需要返回当前Context被取消的时间,也就是截止时间。
    • Done():需要返回一个channel,该channel会在工作完成或者Context被取消时关闭,多次调用Done方法返回的是同一个channel。
    • Err():用于返回当前Context结束的原因,仅在Done方法返回的channel被关闭时才返回非空值,这里包含两种错误,如果当前Context被取消,则返回Canceled错误;如果当前Context超时,则返回DeadlineExceeded错误。
    • Value():用来取得当前Context上绑定的值,是一个键值对,所以参数是一个key值,多次调用该方法而参数相同的话,返回的结果也相同。

            虽然Context是一个接口,但是标准包里面实现了其他的两个方法:Background方法和TODO方法,可通过这两个方法来使用Context。在介绍这两个方法之前,需要先介绍一下Context的实现。

            Context在数据结构上是一种单向继承关系,最开始的Context起到类似于初始化的作用,里面有一些数据,下一层的Context会继承上一层的Context,新的Context可以有children,children就是在上一层的Context外面再套一层,新扩的一层可以存储与自己相关的数据。这种多层结构可以像启动goroutine一样扩展很多层。

            理解了Context的分层模式,就可以方便地理解Background和TODO方法了,这两个方法用于返回私有化的变量background和todo,这两个变量就存储于最顶层的parent Context中,后续的Context都是衍生自这个parent,形成树状层次。当一个parent Context被取消时,继承自它的所有Context都会被取消。

            下面来看一下这两个方法在源码中的实现:

    1. func Background() Context {
    2. return background
    3. }
    4. func TODO() Context {
    5. return todo
    6. }

            background和todo两个私有变量是在context包初始化的时候就定义好的,Background和TODO这两个方法也没有什么差别,可以理解为二者互为别名,只是Background方法是每个Context的顶层默认值,用于main函数,以及初始化、测试等代码中,它作为根Context是不可以被取消的。而TODO方法则是在不确定的时候使用的,但现实中很少使用。

            background和todo这两个私有变量其实是两个指针,指向emptyCtx结构体实例。emptyCtx的定义如下: 

    1. type emptyCtx int
    2. func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    3. return
    4. }
    5. func (*emptyCtx) Done() <-chan struct{} {
    6. return nil
    7. }
    8. func (*emptyCtx) Err() error {
    9. return nil
    10. }
    11. func (*emptyCtx) Value(key interface{}) interface{} {
    12. return nil
    13. }

            可以看到,本质上background和todo是不携带任何信息的Context,不可取消,没有截止时间;而衍生出来的Context都继承自这个根Context。 

    2.context退出与传递

            前面介绍了Context的分层模式,那么Context是如何实现退出和传递的呢?退出和传递靠context包提供的With系列函数实现的。下面来看一下With系列函数,一共有四个:

    1. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {}
    2. func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {}
    3. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {}
    4. func WithValue(parent Context, key, val interface{}) Context {}

            这些函数都接收一个Context类型的参数parent,并返回一个Context类型的值,这样就层层创建出不同的节点。父节点创建Context,并传递给子节点。下面介绍一下各个函数的作用。

    • WithCancel:parent Context根据参数创建一个新的children Context,同时还返回一个取消该children Context的函数CancelFunc。其主要作用是在parent和children之间同步取消或结束信号,确定parent被取消时,其children也会收到信号而被取消。其实现的原理是所有的children都被保存在一个map中,如果是Context执行了Done方法会返回done channel,此时是正常结束所以返回以后就完结了;而如果是通过Err方法结束,则会遍历Context的所有children并关闭其channel。
    • WithDeadline:与WithCancel类似,指定一个截止时间参数,到了截止时间会自动取消该Context。当截止时间发生后,子context将退出。因此子context的退出有3种时机,一种是父context退出;一种是超时退出;一种是主动调用cancel函数退出。
    • WithTimeout:与WithDeadline类似,指定超时时间。
    • WithValue:该函数的作用是生成一个绑定了一个键值对数据的Context,这个绑定的数据可以通过该Context的Value方法访问。该方法可以完成追踪功能,需要通过Context传递数据时可以使用该方法。

            Context对象的生存周期一般仅为一个请求的处理周期,即针对一个请求创建一个Context变量(它是上下文树结构的根)。在请求处理结束后,撤销此变量,释放资源。

            每次创建一个协程时,可以将原有的上下文传递给这个子协程,或者新创建一个子上下文传递给这个协程。上下文能灵活地存储不同类型、不同数目的值,并且使多个协程安全地读写其中的值。当通过父Context对象创建子上下文对象时,即可获得子上下文的一个取消函数,这样父上下文对象的创建环境就获得了对子上下文的撤销权。

            在协程中,childCtx是preCtx的子context,其设置的超时时间为300ms。但是preCtx的超时时间为100 ms,因此父context退出后,子context会立即退出,实际的等待时间只有100ms。

    三、context应用

            在平时协程控制当中,我们常见应用采用通道、上下文以及sync包,通过这三者,完全可以达到完美控制协程运行的目的。

    • 使用sync.WaitGroup,它用于线程总同步,会等待一组线程集合完成,才会继续向下执行,这对监控所有子协程全部完成的情况特别有用,但要控制某个协程就无能为力了。
    • 使用通道来传递消息,一个协程发送通道信号,另一个协程通过select得到通道信息,这种方式可以满足协程之间的通信,控制协程运行。但如果协程数量达到一定程度,就很难把控了。或者这两个协程还和其他协程也有类似通信,例如A与B,B与C,如果A发信号B退出了,C有可能等不到B的通道信号而被遗忘。
    • 使用上下文来传递消息,上下文是层层传递机制,根节点完全控制了子节点,根节点(父节点)可以根据需要选择自动还是手动结束子节点。而每层节点所在的协程就可以根据信息来决定下一步的操作。

            这里主要介绍使用上下文控制协程的运行,两个协程都可以收到cancel()发出的信号,B方法不结束协程可反复接收取消信息。

    1. package main
    2. import (
    3. "context"
    4. "fmt"
    5. "log"
    6. "os"
    7. "os/signal"
    8. "syscall"
    9. "time"
    10. )
    11. func A(ctx context.Context) string {
    12. ctx = context.WithValue(ctx, "funcA", "A")
    13. go B(ctx)
    14. // 监听上层的ctx
    15. select {
    16. case <-ctx.Done():
    17. fmt.Println("A Done")
    18. default:
    19. log.Println("func A: default")
    20. }
    21. return "AA"
    22. }
    23. func B(ctx context.Context) string {
    24. ctx = context.WithValue(ctx, "funcB", "B")
    25. go C(ctx)
    26. // 监听自己上层的ctx
    27. select {
    28. case <-ctx.Done():
    29. fmt.Println("B Done")
    30. return "B"
    31. default:
    32. log.Println("func B: default")
    33. }
    34. return "BB"
    35. }
    36. func C(ctx context.Context) string {
    37. ctx = context.WithValue(ctx, "funcCA", ctx.Value("funcA"))
    38. ctx = context.WithValue(ctx, "funcCB", ctx.Value("funcB"))
    39. // 监听自己上层的ctx
    40. select {
    41. case <-ctx.Done():
    42. fmt.Println("C Done")
    43. return "C"
    44. default:
    45. log.Println("func C: default")
    46. }
    47. return "CC"
    48. }

            执行结果如下:

             

            下面代码用上下文嵌套控制3个协程A,B,C。在主程序发出cancel信号后,每个协程都能接收根上下文的Done()信号而退出。

    1. package main
    2. import (
    3. "context"
    4. "fmt"
    5. "log"
    6. "os"
    7. "os/signal"
    8. "syscall"
    9. "time"
    10. )
    11. func A(ctx context.Context) string {
    12. ctx = context.WithValue(ctx, "funcA", "A")
    13. go B(ctx)
    14. // 监听上层的ctx
    15. select {
    16. case <-ctx.Done():
    17. fmt.Println("A Done")
    18. default:
    19. log.Println("func A: default")
    20. }
    21. return "AA"
    22. }
    23. func B(ctx context.Context) string {
    24. ctx = context.WithValue(ctx, "funcB", "B")
    25. go C(ctx)
    26. // 监听自己上层的ctx
    27. select {
    28. case <-ctx.Done():
    29. fmt.Println("B Done")
    30. return "B"
    31. default:
    32. log.Println("func B: default")
    33. }
    34. return "BB"
    35. }
    36. func C(ctx context.Context) string {
    37. ctx = context.WithValue(ctx, "funcCA", ctx.Value("funcA"))
    38. ctx = context.WithValue(ctx, "funcCB", ctx.Value("funcB"))
    39. // 监听自己上层的ctx
    40. select {
    41. case <-ctx.Done():
    42. fmt.Println("C Done")
    43. return "C"
    44. default:
    45. log.Println("func C: default")
    46. }
    47. return "CC"
    48. }
    49. func main() {
    50. // 新建一个ctx
    51. timeout := time.Second * 5
    52. ctx, _ := context.WithTimeout(context.Background(), timeout)
    53. log.Println("funcA 执行完成,返回:", A(ctx))
    54. select {
    55. case <-ctx.Done():
    56. log.Println("context Done")
    57. break
    58. }
    59. // 监听系统退出信号
    60. quit := make(chan os.Signal, 1)
    61. signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    62. <-quit
    63. }

            执行结果如下(打印结果不一定是这样顺序输出):

  • 相关阅读:
    利用C++开发一个迷你的英文单词录入和测试小程序-源码
    Python爬虫爬图片测试1
    监控、无人机摄像头RTSP协议对接腾讯云直播
    以太坊硬分叉愈演愈烈:为了分叉而分叉or保全矿工利益?
    Android Compose 文本输入框TextField使用详解
    50道基础数据结构面试题
    美国访问学者博士后面签常见问题
    vue2添加自定义节流指令
    基于matlab的复杂背景下不规则目标边缘提取算法的仿真
    成为会带团队的技术人 在管理艺术中寻找确定性的“工程逻辑”
  • 原文地址:https://blog.csdn.net/qq_34272964/article/details/127099996