• GolangContext扫盲与原理解析


    Golang-Context扫盲与原理解析

    一.什么是Context?

    • context是一个包,是Go1.7引入的标注库,中文译做上下文,准确的说是goroutine的上下文,包含goroutine的运行状态,环境,现场等信息。
    • context主要用于在goroutine之间传递上下文信息,比如取消信号,超时时间,截止时间,kv等。

    二.为什么要有Context?

    在Go中,控制并发有两种经典的方式,一个是WaitGroup,另外一个就是context

    • WaitGroup:控制多个groutine同时完成,这是等待的方式,等那些必要的goroutine都工作完了我才能工作
    • Context:主动通知某一个groutine结束,这是主动通知的方式,通知某些groutine你不要再工作了

    其实主动通知的方式,除了context,还有一种方式也可以实现

    • channle + select
    func main() {
    	stop := make(chan bool)
    
    	go func() {
    		for {
    			select {
    			case <-stop:
    				fmt.Println("监控退出,停止了...")
    				return
    			default:
    				fmt.Println("goroutine监控中...")
    				time.Sleep(2 * time.Second)
    			}
    		}
    	}()
    
    	time.Sleep(10 * time.Second)
    	fmt.Println("可以了,通知监控停止")
    	stop<- true
    	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
    	time.Sleep(5 * time.Second)
    }
    

    采用channle + select 这种方式来实现主动通知,有两个致命的缺点:

    • 只能通知一个groutine结束,无法应对很多goroutine都需要结束的情况
    • 无法应对goroutine又衍生出其他更多的goroutine的情况

    上述这两种场景其实在业务中非常的常见

    • 场景1:比如一个网络请求Request,每个Request都需要开启一个goroutine做一些事情,这些goroutine又可能会开启其他的goroutine,具体表现在Go的Server中,通常每一个请求都会启动若干个goroutine同时工作,有些去数据库拿数据,有些调用下游接口获取相关数据,这些goroutine需要共享这个请求的基本数据,例如登录token,处理请求的最大超时时间等等,当请求被取消或是处理时间太长,这时,所有正在为这个请求工作的goroutine都需要快速退出,因为他们的工作成果不再被需要了

    为应对上述场景,并且使得goroutine是可追踪的,context应运而生

    三.Context 如何使用?

    1.context控制多个goroutine

    func main() {
    	ctx, cancel := context.WithCancel(context.Background())
    	go watch(ctx,"【监控1】")
    	go watch(ctx,"【监控2】")
    	go watch(ctx,"【监控3】")
    
    	time.Sleep(10 * time.Second)
    	fmt.Println("可以了,通知监控停止")
    	cancel()
    	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
    	time.Sleep(5 * time.Second)
    }
    
    func watch(ctx context.Context, name string) {
    	for {
    		select {
    		case <-ctx.Done():
    			fmt.Println(name,"监控退出,停止了...")
    			return
    		default:
    			fmt.Println(name,"goroutine监控中...")
    			time.Sleep(2 * time.Second)
    		}
    	}
    }
    

    上述样例,context控制了三个goroutine,当context cancle之后,这三个goroutine便都退出了

    2.传递共享数据

    package main
    
    import (
    	"context"
    	"fmt"
    )
    
    func main() {
    	ctx := context.Background()
    	process(ctx)
    
    	ctx = context.WithValue(ctx, "traceId", "qcrao-2019")
    	process(ctx)
    }
    
    func process(ctx context.Context) {
    	traceId, ok := ctx.Value("traceId").(string)
    	if ok {
    		fmt.Printf("process over. trace_id=%s\n", traceId)
    	} else {
    		fmt.Printf("process over. no trace_id\n")
    	}
    }
    

    3.取消goroutine,防止goroutine泄露

    func gen(ctx context.Context) <-chan int {
        ch := make(chan int)
        go func() {
            var n int
            for {
                select {
                case <-ctx.Done():
                    return
                case ch <- n:
                    n++
                    time.Sleep(time.Second)
                }
            }
        }()
        return ch
    }
    
    func main() {
        ctx, cancel := context.WithCancel(context.Background())
        defer cancel() // 避免其他地方忘记 cancel,且重复调用不影响
    
        for n := range gen(ctx) {
            fmt.Println(n)
            if n == 5 {
                cancel()
                break
            }
        }
        // ……
    }
    

    如果只需要五个整数,在n==5时,直接break了没有cancle,那么就会存在goroutine泄露的问题!

    四.Context 底层原理解析

    1.Context的接口分析和实现

    1,接口分析

    type Context interface {
    	Deadline() (deadline time.Time, ok bool)
    
    	Done() <-chan struct{}
    
    	Err() error
    
    	Value(key interface{}) interface{}
    }
    
    • Deadline() : 获取设置的截止时间,第一个返回值是截止时间,到底这个时间点,context会自动发起取消请求,第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话需要调用函数cancle进行取消
    • Done() : 返回一个只读的chan,类型为struct{},我们在goroutine中,如果此方法返回的chan可读,则意味着parent.context已发起取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源
    • Err() : 返回取消的错误原因,即因为什么context被取消
    • Value(key) : 返回该context上绑定的值,是kv键值对,线程安全

    它们都是幂等的。也就是说连续多次调用同一个方法,得到的结果都是相同的。

    经典用法如下:

    func Stream(ctx context.Context, out chan<- Value) error {
      	for {
      		v, err := DoSomething(ctx)
      		if err != nil {
      			return err
      		}
      		select {
      		case <-ctx.Done():
      			return ctx.Err()
      		case out <- v:
      		}
      	}
      }
    

    2.接口实现

    context根据其父子context关系,可以抽象成一颗树,节点就是context

    context接口并不需要我们实现,GO已经内置了两个了,可以使用这两个做完最顶层的父context,从而衍生出更多的子context

    内置的根context:

    • background : 主要用于main函数,初始化以及测试代码中,作为context这个树结构的最顶层根context
    • todo : 目前还不知道具体的使用场景,当你也不知道应该使用什么context的时候,可以使用这个
    var (
    	background = new(emptyCtx)
    	todo       = new(emptyCtx)
    )
    
    func Background() Context {
    	return background
    }
    
    func TODO() Context {
    	return todo
    }
    

    background和todo二者的本质都是emptyCtx结构

    • emptyCtx:一个不可取消,没有设置截止时间,没有携带任何值的Context
    type emptyCtx int
    
    func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    	return
    }
    
    func (*emptyCtx) Done() <-chan struct{} {
    	return nil
    }
    
    func (*emptyCtx) Err() error {
    	return nil
    }
    
    func (*emptyCtx) Value(key interface{}) interface{} {
    	return nil
    }
    

    2.Context接口和类型间的关系

    类图如下:

    图来自此:https://blog.csdn.net/kevin_tech/article/details/119901843

    通过上面的类图,我们可以获取以下信息:

    • 除了Context接口外还定义了一个叫做canceler的接口,带取消功能的Context canclerCtx便是实现了这个接口
    • emptyCtx 什么属性也没有,啥也不能干
    • valueCtx 只能携带一个键值对,且自身要已付在上一级的Context上
    • timerCtx 继承自canclerCtx 他们都是带取消功能的Context
    • 除了emptyCtx,其他类型的Context都依附在上级Context上

    3.Context的继承衍生

    有了根Context,那么如何衍生出更多的子Context呢?这个就要靠Context包为我们提供的With系列函数了

    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, val interface{}) Context
    

    这四个with系列的函数,都有一个parent参数,也就是父Context,我们要基于这个父Context创建出子Context,可以理解为子Context对父Context的继承,也可以理解为基于父Context的衍生。这四个with系列的函数,只是创建子Context的条件不同而已

    通过这四个函数,我们就可以创建出一颗Context树,树的每个节点都可以有多个任意的子节点,节点的层级可以有任意多个

    • WithCancle : 传入一个父Context,返回一个子Context以及一个取消函数(用来取消Context)

    • WithDeadline : 传入一个父Context和一个截止时间,同样返回一个子Context和一个取消函数,意味着到了这个截止时间,会自动取消Context,当然我们也可以通过取消函数提前进行取消

    • WithTimeout : 和WithDeadline差不多,表示超时自动取消,是多少时间后自动取消Context的意思

    • WithValue : 和取消Context无关,它是为了生成绑定了一个键值对数据的Context,这个数据可通过Context.value访问

    可以注意到上述几个函数都会返回一个取消函数,CancelFunc

    • CancelFunc: 取消一个Context,以及这个Context节点下的所有子Context,不管有多少层,不管有多少数量

    4.Context的数据传递与使用

    • 我们通过context.WithValue函数生成一个context,通过.Value函数获取Context键值对的值
    var key string="name"
    
    func main() {
    	ctx, cancel := context.WithCancel(context.Background())
    	//附加值
    	valueCtx:=context.WithValue(ctx,key,"【监控1】")
    	go watch(valueCtx)
    	time.Sleep(10 * time.Second)
    	fmt.Println("可以了,通知监控停止")
    	cancel()
    	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
    	time.Sleep(5 * time.Second)
    }
    
    func watch(ctx context.Context) {
    	for {
    		select {
    		case <-ctx.Done():
    			//取出值
    			fmt.Println(ctx.Value(key),"监控退出,停止了...")
    			return
    		default:
    			//取出值
    			fmt.Println(ctx.Value(key),"goroutine监控中...")
    			time.Sleep(2 * time.Second)
    		}
    	}
    }
    

    在上面的样例中,我们生成了一个新的Context,这个新的Context带有这个键值对,在使用的时候,可以通过Value的方法读取,ctx.Value(key)

    五.Context FQA

    1.Context使用事项

    在官方博客里,对于使用 context 提出了几点建议:

    • Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx.
    • Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
    • Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
    • The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

    翻译一下:

    • 不要将Context塞到结构体里,直接将Context类型作为函数的第一参数,而且一般都命名为ctx
    • 不要向函数传入一个nil的Context,如果你实在不知道传什么,标准库给你准备好了一个context:todo
    • 不要把本应该作为函数参数的数据塞入到context中,context存储的应该是一些共同数据,比如登录的session,cookie等
    • 同一个context可能会被传递到多个goroutine,别担心,context是并发安全的

    2.到底有几类的Context?

    • 类型一,emptyCtx,Context的源头
    • 类型二,cancelCtx,cancle机制的灵魂
    • 类型三,timerCtx,cancle机制场景的补充
    • 类型四,valueCtx,传值需要

    这几类的context组成了一颗context树!

    3.context存储值的底层是一个Map吗?

    • 不是
    • 每一个KV映射都对应一个valueCtx,是一个个节点,当传递多个值时就要构建多个valueCtx,同时这也是context不能从底向上传递值的原因
    • 在调用value获取键值对的值的时候,会首先在本context寻找对应key,如果没有找到则会在父context中递归寻找

    4. Context 是如何实现数据共享的?

    图来自:https://blog.csdn.net/kevin_tech/article/details/119901843

    • 数据共享即:元数据在任务间的传递
    • 其实现的Value方法能够在整个Context树链路上查找指定键的值,直到回溯到根Context,也就是emptyCtx,这也是emptyCtx什么功能也不提供的原因,因为他是作为根节点而存在的。
    • 每次要在Context链路上增加携带的KV时,都要在上级Context的基础上新建一个ValueCtx存储KV,而且只能增加不能修改,读取KV也是一个幂等操作,所以Context就这样实现了并发安全的数据共享机制,并且全程无锁,不会影响性能

    5. Context 是如何实现以下三点的?

    • 上层任务取消后,所有的下层任务都会被取消
    • 中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务已经同级任务

    分析如下:

    • 首先在 创建带取消功能的Context时还是要在父Context节点的基础上创建,从而保持整个Context链路的连续性,除此之外,还会在Context链路中找到上一个带取消功能的Context,把自己加入到他的children列表里,这样在整个Context链路中,除了父子Context之间有之间关联外,可取消的Context还会通过维护自身携带的Children属性建立与自己下级可取消的Context的关联,具体可参考下图

    图来自:https://blog.csdn.net/kevin_tech/article/details/119901843

    • 通过上图的这种设计,如果要在整个任务链路上取消某个canclerCtx时,就既能做到取消自己,也能通知下级CancelCtx进行取消,同时还不会影响到上级和同级的其他节点。

    五.总结

    context主要用于父子goroutine之间同步取消信号,本质上是一种协程的调度方式,另外有两点需要注意:

    • context的取消操作是无侵入的,上游任务仅仅使用context通知下游任务不再被需要,但不会直接干涉下游任务的执行,由下游任务自己决定后续的操作。

    • context是并发安全的,因为context本身是不可变的,可以放心在多个goroutine间传递

    参考:

  • 相关阅读:
    配置ssh免密登录
    idea打开项目定位到选择项目的位置
    MySQL decimal、numeric数据类型介绍
    C++ string::size_type类型
    Java Oracle存储过程问题
    PLSQL Developer简单使用教程
    ed2k如何下载
    老毛桃U盘启动盘制作工具安装教程
    Hibernate current_session_context_class的事务说明
    java openSession和getCurrentSession的比较
  • 原文地址:https://www.cnblogs.com/yinbiao/p/15799341.html
Copyright © 2020-2023  润新知