• go并发之context


    0.引言

    Context的主要作用:
    1.上下文信息传递 ,比如处理 http 请求、在链路追踪中传递信息;
    2.控制子 goroutine 的运行;
    3.超时控制的方法调用;
    4.可以取消的方法调用。

    1.用法
    
    type Context interface {
        Deadline() (deadline time.Time, ok bool)
        Done() <-chan struct{}
        Err() error
        Value(key interface{}) interface{}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    - Done会返回一个channel,当该context被取消的时候,该channel会被关闭,同时对应的使用该context的routine也应该结束并返回。
    - Context中的方法是协程安全的,这也就代表了在父routine中创建的context,可以传递给任意数量的routine并让他们同时访问。
    - Deadline会返回一个超时时间,routine获得了超时时间后,可以对某些io操作设定超时时间。
    - Value可以让routine共享一些数据,当然获得数据是协程安全的。

    接下来,我会介绍标准库中几种创建特殊用途 Context 的方法:WithValue、WithCancel、WithTimeout 和 WithDeadline,包括它们的功能以及实现方式。要创建context树,第一步是要有一个根结点。context.Background函数的返回值是一个空的context,经常作为树的根结点,它一般由接收请求的第一个routine创建,不能被取消、没有值、也没有过期时间。

    func Background() Context
    
    • 1

    之后该怎么创建其它的子孙节点呢?context包为我们提供了以下函数:

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

    WithCancel,当顶层的Request请求处理结束,或者外部取消了这次请求,就可以cancel掉顶层context,从而使整个请求的routine树得以退出。不是只有你想中途放弃才去调用 cancel,而是只要你的任务正常完成了,就需要调用 cancel,这样,这个 Context 才能释放它的资源。

    WithDeadlineWithTimeoutWithCancel多了一个时间参数,它指示context存活的最长时间。如果超过了过期时间,会自动撤销它的子context。所以context的生命期是由父context的routine和deadline共同决定的。

    WithValue返回parent的一个副本,该副本保存了传入的key/value,而调用Context接口的Value(key)方法就可以得到val。注意在同一个context中设置key/value,若key相同,值会被覆盖。

    传值示例:
    package main
    
    import (
    	"context"
    	"fmt"
    )
    
    func func1(ctx context.Context) {
    	ctx = context.WithValue(ctx, "k1", "v1")
    	func2(ctx)
    }
    func func2(ctx context.Context) {
    	fmt.Println(ctx.Value("k1").(string))
    }
    
    func main() {
    	ctx := context.Background()
    	func1(ctx)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    主动取消示例:
    package main
    
    import (
    	"context"
    	"errors"
    	"fmt"
    	"sync"
    	"time"
    )
    
    func func1(ctx context.Context, wg *sync.WaitGroup) error {
    	defer wg.Done()
    	respC := make(chan int)
    	// 处理逻辑
    	go func() {
    		time.Sleep(time.Second * 5)
    		respC <- 10
    	}()
    	// 取消机制
    	select {
    	case <-ctx.Done():
    		fmt.Println("cancel")
    		return errors.New("cancel")
    	case r := <-respC:
    		fmt.Println(r)
    		return nil
    	}
    }
    
    func main() {
    	wg := new(sync.WaitGroup)
    	ctx, cancel := context.WithCancel(context.Background())
    	wg.Add(1)
    	go func1(ctx, wg)
    	time.Sleep(time.Second * 2)
    	// 触发取消
    	cancel()
    	// 等待goroutine退出
    	wg.Wait()
    }
    
    • 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
    超时取消示例
    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    )
    
    func func1(ctx context.Context) {
    	hctx, hcancel := context.WithTimeout(ctx, time.Second*4)
    	defer hcancel()
    
    	resp := make(chan struct{}, 1)
    	// 处理逻辑
    	go func() {
    		// 处理耗时
    		time.Sleep(time.Second * 10)
    		resp <- struct{}{}
    	}()
    
    	// 超时机制
    	select {
    	//	case <-ctx.Done():
    	//		fmt.Println("ctx timeout")
    	//		fmt.Println(ctx.Err())
    	case <-hctx.Done():
    		fmt.Println("hctx timeout")
    		fmt.Println(hctx.Err())
    	case v := <-resp:
    		fmt.Println("test2 function handle done")
    		fmt.Printf("result: %v\n", v)
    	}
    	fmt.Println("test2 finish")
    	return
    
    }
    
    func main() {
    	ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
    	defer cancel()
    	func1(ctx)
    }
    
    • 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
    • 41
    • 42

    在使用 Context 的时候,有一些约定俗成的规则。
    1.一般函数使用 Context 的时候,会把这个参数放在第一个参数的位置。
    2.从来不把 nil 当做 Context 类型的参数值,可以使用 context.Background() 创建一个空的上下文对象,也不要使用 nil。
    3.Context 只用来临时做函数之间的上下文透传,不能持久化 Context 或者把 Context 长久保存。把 Context 持久化到数据库、本地文件或者全局变量、缓存中都是错误的用法。
    4.key 的类型不应该是字符串类型或者其它内建类型,否则容易在包之间使用 Context 时候产生冲突。使用 WithValue 时,key 的类型应该是自己定义的类型。
    5.常常使用 struct{}作为底层类型定义 key 的类型。对于 exported key 的静态类型,常常是接口或者指针。这样可以尽量减少内存分配。

    目前收集的关于Context存在的问题主要有:
    1.Context 包名导致使用的时候重复 ctx context.Context;
    2.Context.WithValue 可以接受任何类型的值,非类型安全;
    3.Context 包名容易误导人,实际上,Context 最主要的功能是取消 goroutine 的执行;
    4.Context 漫天飞,函数污染。

    2.参考

    Go Concurrency Patterns: Context:https://go.dev/blog/context
    Go: Context and Cancellation by Propagation:https://medium.com/a-journey-with-go/go-context-and-cancellation-by-propagation-7a808bbc889c
    剖析 Golang Context:从使用场景到源码分析:https://xie.infoq.cn/article/3e18dd6d335d1a6ab552a88e8
    Go Context的踩坑经历:https://zhuanlan.zhihu.com/p/34417106?hmsr=toutiao.io
    Go官方文档:https://pkg.go.dev/context
    极客时间go并发编程实战课

  • 相关阅读:
    化工&python | CSTR连续搅拌反应器系统
    ffplay源码分析:音频重采样
    适合计算机编程开发的笔记本电脑推荐
    chromium114添加新的语言国际化支持
    layui2.9.7-入门初学
    盘点3种Python网络爬虫过程中的中文乱码的处理方法
    IO流的原理以及分类
    深度解析 slab 内存池回收内存以及销毁全流程
    Httpd启动报错 Couldn‘t create the ssl-cache 的解决办法
    现代数据栈:高效开发数据,辅助企业决策
  • 原文地址:https://blog.csdn.net/wen3qin/article/details/126705427