• Go context 原理(channel广播机制 + mutex线程安全)


    Go context 原理简述

    context包构建了树型关系的Context。go Context底层实际上是通过使用 channel + mutex 来实现的。channel负责在父级节点cancel()后的相关子协程之间广播通信,而mutex则保证了ctx在多个 goroutine 之间传递时的线程安全。

    使用context时,首先要创建一个顶级的context,也就是context.Background()

    每次用户请求到来时,向一组具有上下文关系的 goroutine 中分别传入ctx 参数,并分别监听ctx.Done()方法。

    Done()方法返回一个只读的channel,所有相关函数监听此channel。一旦channel关闭,所有负责监听的 goroutine 通过Go channel被关闭时的广播机制,都能够收到通知。

    子goroutine可以通过 select-case 的方式检查自身是否被父级节点cancel(),一旦上层环境(父节点)撤销了本 goroutine 的执行,应当终止对当前请求信息的处理,释放资源并return。

    正因为上述方式,一个request范围内所有 goroutine 运行时的取消能得到有效控制。

    Go context 介绍

    • 概念:
      Go 1.7 标准库引入 context,中文译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。
    • 场景:
      后端接收请求时,有时要将获取到的数据交由多个协程处理。例如登录验证时,将权限验证、密码验证、有效期验证分到三个不同的协程里处理,如果此时有一个协程处理失败了,其他协程也应该立即关闭,避免持续占用系统资源。而在Go中就可以用context来进行控制操作。
    • 作用:
      context 主要用来在一组 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。
    • 比较:
      在Go里,我们不能直接杀死协程,协程的关闭一般会用 channel + select 方式来控制。但在某些场景下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline 等,而且可以同时被关闭。再用 channel + select 就会比较麻烦,这时就可以通过 context 来实现。
    • 使用:
      • 顶层Context:Background()
        此方法返回一个空的Context,它作为所有由此继承Context的根节点。
        要创建Context树,首先就是要创建根节点。该Context通常由接收request的第一个goroutine创建。根节点不能被取消、没有值、也没有过期时间,常作为处理request的顶层context存在。
      • context库中,有4个关键方法:
        WithCancel()返回一个子context对象和一个cancel()函数,可以主动停止goroutine。
        WithDeadline()设置一个时间点,到点后执行cancel()方法。
        WithTimeout()设置一个time.Duration,到时间则会cancel这个context。
        WithValue()可以设置一个 k/v 的键值对,可在下游任何一个嵌套的context中通过key获取value,但是不建议使用这种来做goroutine之间的通信。这个值一般是线程安全的.
      • 再配合Context提供的Done()方法,子goroutine可以通过 select-case 的方式检查自身是否被父级节点Cancel,一旦上层环境(父)撤销了本goroutine的执行,应当终止对当前请求信息的处理,释放资源并return:
    // (1) 顶层Context:Background():返回一个空的Context,它作为所有由此继承Context的根节点
    func Background() Context {
    }
    
    // (2) context库中,有4个关键方法
    // 带cancel返回值的Context,一旦cancel被调用,即取消该创建的context
    func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    }
    
    // 带有效期cancel返回值的Context,即必须到达指定时间点调用的cancel方法才会被执行
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
    }
    
    // 带超时时间cancel返回值的Context,类似Deadline,前者是时间点,后者为时间间隔
    // 相当于WithDeadline(parent, time.Now().Add(timeout)).
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    }
    
    // (3) Context提供的 Done()方法
    select { 
        case <-ctx.Done(): 
            // do some clean… 
            // 主动终止对当前请求信息的处理,释放资源并返回
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    注意:父节点Context可以主动通过调用cancel方法取消子节点Context,而 子节点Context只能被动等待。同时父节点Context自身一旦被取消(如其上级节点Cancel),其下的所有子节点Context均会自动被取消。

    • 代码示例:
      通过引入Context包,一个request范围内所有goroutine运行时的取消能得到有效控制
    package main
    
    import (
        "context"
        "fmt"
        "time"
    )
    
    func someHandler() {
        // 创建继承Background的子节点Context
        ctx, cancel := context.WithCancel(context.Background())
        // ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second) // 3秒后提前结束doSth()
        go doSth(ctx)
    
        //模拟程序运行 - Sleep 5秒
        time.Sleep(5 * time.Second) // 避免main()函数会提前结束
        cancel()
    }
    
    //每1秒work一下,同时会判断ctx是否被取消,如果是就退出
    func doSth(ctx context.Context) {
        var i = 1
        for {
            time.Sleep(1 * time.Second)
            select {
            case <-ctx.Done():
                fmt.Println("done")
                return
            default:
                fmt.Printf("work %d seconds. \n", i)
            }
            i++
        }
    }
    
    func main() {
        fmt.Println("start...")
        someHandler()
        fmt.Println("end.")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 使用原则:
      Context使用原则:

      • 不要把Context放在结构体中,要以参数的方式传递
      • 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位
      • 给一个函数方法传递Context时,不要传递nil,如果不知道传递什么,就使用context.TODO()
      • Context的Value相关方法应该传递必须的参数,不要什么数据都使用这个传递
      • Context是线程安全的,可以放心的在多个goroutine中传递
    • 缺点:
      一旦代码中某处用到了Context,传递Context变量(通常作为函数的第一个参数)会像病毒一样蔓延在各处调用它的地方。即每一个相关函数都必须增加一个context.Context类型的参数,且作为第一个参数,这对无关代码完全是侵入式的。

  • 相关阅读:
    正点原子嵌入式linux驱动开发——异步通知
    Docker-Windows安装使用
    springboot+vue校园篮球比赛预约报名平台java maven
    2023.11.09 homework
    【测开求职】面试题:Redis 吐血整理
    从零开始:Django项目的创建与配置指南
    python+django网吧会员管理系统
    Linux命令之常用基础命令备查手册
    测试经理应该怎么写测试部门年终总结报告?
    MCU软核 1. Altera FPGA上运行8051
  • 原文地址:https://blog.csdn.net/qq_37102984/article/details/127910000