• Go语言类库-context


    定义

    context,中文译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。是一种并发安全在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等的类型。

    使用场景

    context用来解决goroutine之前退出通知、元数据传递的功能的问题。

    定时取消

    可以通过WithDealine()和WithTimeout()来获得一个到达某个时间点取消当前goroutine以及子goroutine的context;当然也可以在上层调用返回的cancel方法取消子goroutine。

    package main
    
    func main(){
        ctx,cancel := WithTimeout(context.Background(),1*time.Second)
        defer cancel()
        res := process(ctx)
        fmt.Println(res)
    }
    func process(ctx context.Context)int{
        time.Sleep(3*time.Second)
        return 1
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    防止goroutine泄露

    前面那个例子里,goroutine 还是会自己执行完,最后返回,只不过会多浪费一些系统资源。这里改编一个“如果不用 context 取消,goroutine 就会泄漏的例子”。

    func main() {
    	for n := range gen() {
    		fmt.Println(n)
    		if n == 5 {
    			break
    		}
    	}
    	// ……
    }
    func gen() <-chan int {
    	ch := make(chan int)
    	go func() {
    		var n int
    		for {
    			ch <- n
    			n++
    			time.Sleep(time.Second)
    		}
    	}()
    	return ch
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    gen是一个可以生成无限整数的协程,但如果我只需要它产生的前 5 个数,那么就会发生 goroutine 泄漏。当 n == 5 的时候,直接 break 掉。那么 gen 函数的协程就会执行无限循环,永远不会停下来。发生了 goroutine 泄漏。

    使用context优化:

    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
    		}
    	}
    	// ……
    }
    
    • 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

    增加一个 context,在 break 前调用 cancel 函数,取消 goroutine。gen 函数在接收到取消信号后,直接退出,系统回收资源。

    传递共享数据

    使用WithValue()可以往返回一个指协程,并且写入了一对key-value。

    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")
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    注意事项

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

    context底层原理

    context是一个官方包,包中Context是一个接口包含Deadline(),Done(),Err(),value()四个方法;还有一个canceler接口,包含cancel()和Done()2个方法。cancelCtx结构体实现了canceler接口,可以通过WithCancel()方法返回一个cancelCtx对象和取消方法,调用取消方法可以关闭该channel的done channel。timerCtx结构体是基于cancelCtx的,只是多了一个time.Timer和一个deadline,WithDeadline()返回的conctext在未来什么时候会取消,WhithTimeout()调用了WithDeadline(),会把当前时间加上时间间隔。valueCtx也是一个结构体,有key和value两个字段,通过WithValue()方法可以创建一个context存放了key-value,且父节点是传入的context。查找context过程就是从子节点往上查找,直到key匹配上。

    context接口

    type Context interface {
    	// 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
    	Done() <-chan struct{}
    
    	// 在 channel Done 关闭后,返回 context 取消原因
    	Err() error
    
    	// 返回 context 是否会被取消以及自动取消时间(即 deadline)
    	Deadline() (deadline time.Time, ok bool)
    
    	// 获取 key 对应的 value
    	Value(key interface{}) interface{}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    Context 是一个接口,定义了 4 个方法,它们都是幂等的。也就是说连续多次调用同一个方法,得到的结果都是相同的。
    Done() 返回一个 channel,可以表示 context 被取消的信号:当这个 channel 被关闭时,说明 context 被取消了。注意,这是一个只读的channel。 我们又知道,读一个关闭的 channel 会读出相应类型的零值。并且源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个 receive-only 的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(零值)后,就可以做一些收尾工作,尽快退出。
    Err() 返回一个错误,表示 channel 被关闭的原因。例如是被取消,还是超时。
    Deadline() 返回 context 的截止时间,通过此时间,函数就可以决定是否进行接下来的操作,如果时间太短,就可以不往下做了,否则浪费系统资源。当然,也可以用这个 deadline 来设置一个 I/O 操作的超时时间。
    Value() 获取之前设置的 key 对应的 value。

    canceler接口

    type canceler interface {
    	cancel(removeFromParent bool, err error)
    	Done() <-chan struct{}
    }
    
    • 1
    • 2
    • 3
    • 4

    实现了上面定义的两个方法的 Context,就表明该 Context 是可取消的。源码中有两个类型实现了 canceler 接口:*cancelCtx 和 *timerCtx。注意是加了 * 号的,是这两个结构体的指针实现了 canceler 接口。

    Context 接口设计成这个样子的原因:

    • “取消”操作应该是建议性,而非强制性

    caller 不应该去关心、干涉 callee 的情况,决定如何以及何时 return 是 callee 的责任。caller 只需发送“取消”信息,callee 根据收到的信息来做进一步的决策,因此接口并没有定义 cancel 方法。

    • “取消”操作应该可传递

    “取消”某个函数时,和它相关联的其他函数也应该“取消”。因此,Done() 方法返回一个只读的 channel,所有相关函数监听此 channel。一旦 channel 关闭,通过 channel 的“广播机制”,所有监听者都能收到。

    emptyCtx结构体

    emptyCtx是实现了context接口的结构体,不过其每个方法都返回零值。包内TODO和background就是返回emptyCtx的的对象。

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    cancelCtx结构体

    cancelCtx结构体嵌套了context接口(也就是cancelCtx变量实现了context接口),同时他有Done()和cancel()两个成员方法,说明其实现了canceler接口。
    c.done 是“懒汉式”创建,只有调用了 Done() 方法的时候才会被创建。再次说明,函数返回的是一个只读的 channel,而且没有地方向这个 channel 里面写数据。所以,直接调用读这个 channel,协程会被 block 住。一般通过搭配 select 来使用。一旦关闭,就会立即读出零值。
    cancel() 方法的功能就是关闭 channel:c.done;递归地取消它的所有子节点;从父节点从删除自己。达到的效果是通过关闭 channel,将取消信号传递给了它的所有子节点。goroutine 接收到取消信号的方式就是 select 语句中的读 c.done 被选中。

    type cancelCtx struct {
    	Context
    
    	// 保护之后的字段
    	mu       sync.Mutex
    	done     chan struct{}
    	children map[canceler]struct{}
    	err      error
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    通过WithCancel函数可以返回一个新的context和cancelFun,当cancelFun被调用或者父节点done channel被关闭,此context的done channel也会被关闭。

    timerCtx结构体

    timerCtx 基于 cancelCtx,只是多了一个 time.Timer 和一个 deadline。Timer 会在 deadline 到来时,自动取消 context。

    type timerCtx struct {
    	cancelCtx
    	timer *time.Timer // Under cancelCtx.mu.
    
    	deadline time.Time
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    WithDeadline()和WithTimeout()方法可以可以得到一个timerCtx的结构体对象。

    valueCtx结构体

    type valueCtx struct {
    	Context
    	key, val interface{}
    }
    
    • 1
    • 2
    • 3
    • 4

    通过valueCtx函数创建一个valueCtx对象。

    func WithValue(parent Context, key, val interface{}) Context {
    	if key == nil {
    		panic("nil key")
    	}
    	if !reflect.TypeOf(key).Comparable() {
    		panic("key is not comparable")
    	}
    	return &valueCtx{parent, key, val}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    value取值的过程,实际上是一个递归查找的过程:
    它会顺着链路一直往上找,比较当前节点的 key 是否是要找的 key,如果是,则直接返回 value。否则,一直顺着 context 往前,最终找到根节点(一般是 emptyCtx),直接返回一个 nil。所以用 Value 方法的时候要判断结果是否为 nil。
    因为查找方向是往上走的,所以,父节点没法获取子节点存储的值,子节点却可以获取父节点的值。
    WithValue 创建 context 节点的过程实际上就是创建链表节点的过程。两个节点的 key 值是可以相等的,但它们是两个不同的 context 节点。查找的时候,会向上查找到最后一个挂载的 context 节点,也就是离得比较近的一个父节点 context。所以,整体上而言,用 WithValue 构造的其实是一个低效率的链表。

  • 相关阅读:
    一文了解 Go 接口
    计算机网络——DNS协议
    Jmeter介绍与使用
    阿里云对象存储OSS SDK的使用
    岛屿类问题通用解法与DFS框架
    从零开发短视频电商 分布式锁-基于Redis实现
    Exch:移动或清理 Exchange Server 日志文件
    大数据笔记--spark内核解析
    python学习-基础知识总结
    SSM项目实战——哈哈音乐(四)前台模块开发
  • 原文地址:https://blog.csdn.net/jeremy_ke/article/details/127770375