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
}

防止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
}

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

增加一个 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. 不要将 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{}
}

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{}
}

实现了上面定义的两个方法的 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
}

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
}

通过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
}

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

valueCtx结构体

type valueCtx struct {
	Context
	key, val interface{}
}

通过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}
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值