Go并发编程--Context包

为什么使用Context

go的扛把子

要论go最津津乐道的功能莫过于go强大而简洁的并发能力。

func main() {
	go func() {
		fmt.Println("Hello world")
	}()
}

通过简单的go func( ){ },go可以快速生成新的协程并运行。

想象一个没有Context的世界

有并发的地方就有江湖,每个编程语言都有各自的并发编程方式,也有不同的并发控制方法,比如java通过join( )来做主子线程同步。Java里面还有ThreadLocal的概念,但是在go里面也没有。

go里面常用于协程间通信和管理的有channelsync包。比如channel可以通知协程做特定操作(退出, 阻塞)等,sync可以加锁和同步。

Context基础用法

最为重要的就是3个基础能力,取消超时附加值

新建一个Context

ctx := context.TODO()
ctx := context.BackGround()

这两个方法返回的内容都是一样的,都是返回一个空的context,这个context一般用来做父context。

WithCancel

// 函数声明
func WithCancel(parentCtx context.Context) (ctx context.Context, cancel context.CancelFunc){
	// 用法返回一个子Context 和 主动取消函数
	ctx, cancel := context.WithCancel(parentCtx)
}

这个函数相当重要,会根据传入的context生成一个子context和一个取消函数。当父context有相关取消操作,或者直接调用cancel函数的话,子context就会被取消。

举个日常业务中的常用的例子:

// 一般操作比较耗时或者涉及远程调用等,都是在输入参数里带上一个ctx
func Do(parentCtx context.Context) {
	ctx, cancel := context.WithCancel(parentCtx)
	
	// 实现某些逻辑业务
	
	// 当遇到某种条件,比如程序出错,就取消掉子Context
	if err != nil {
		cancel()
	}
}

WithTimeout

// 函数声明
func WithTimeout(parentCtx context.Context, timeout time.Duration) (ctx context.Context, cancel context.CancelFunc) {
	// 用法:返回一个子COntext 和 主动取消函数
	ctx, cancel := context.WithTimeout(parentCtx, time.Second)
}

这个函数在日常工作中使用得非常多,简单来说就是给Context附加一个超时控制,当超时ctx.Done( )返回的channel就能读取到值,协程可以通过这个方式来判断执行时间是否满足要求。

WithDeadLine

// 函数声明
func WithCancel(parentCtx context.Context, d time.Time) (ctx context.Context, cancel context.CancelFunc) {
	// 用法返回一个子Context(会在指定的时间自动取消) 和 主动取消函数
	ctx, cancel := context.WithDeadline(parentCtx, time.Now().Add(5*time.Second))
}

这个函数感觉用得比较少,和WithTimeout相比的话就是使用的是截至时间
示例:

func TestParentContext(t *testing.T) {
	ctx := context.Background()
	dlCtx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Minute))
	childCtx := context.WithValue(dlCtx, "name", 123)
	cancel()
	err := childCtx.Err()
	fmt.Println(err)
}

运行结果:
在这里插入图片描述

WithValue

// 函数声明
func WithValue(parent context.Context, key, val interface{}) {
	// 用法:传入父Context和(key,value),相当于存一个kv
	childCtx := context.WithValue(parent, "name", 123)
	v := childCtx.Value("name")
	fmt.Println(v)
}

这个函数常用来保存一些链路追踪信息,比如API服务里会由来保存一些源IP,请求参数等。
这个方法很常用!

Context源码及接口

Context是一个接口

虽然我们平时写代码时直接context.Context拿来就用,但实际上context.Context是一个接口,源码里是有多种不同的实现的,借此实现不同的功能。


type Context interface {
  // 返回这个ctx预期的结束时间
  Deadline() (deadline time.Time, ok bool)
  // 返回一个channel,当执行结束或者取消时被close,我们平时可以用这个来判断ctx绑定的协程是否该退出。实现里用的懒汉模式,所以一开始可能会返回nil
  Done() <-chan struct{}
  // 如果未完成,返回nil。已完成源码里目前就两种错误,已被取消或者已超时
  Err() error
  // 返回ctx绑定的key对应的value值
  Value(key interface{}) interface{}
}

Context们是一颗树

在这里插入图片描述
context整体是一个树形结构,不同的ctx间可能是兄弟节点或者是父子节点的关系。
同时由于Context接口有多种不同的实现,所以树的节点可能也是多种不同的ctx实现。总的来说我觉得Context的特点是:
1、树形结构,每次调用WithCancelWithValueWithTimeoutWithDeadline实际是为当前节点在追加子节点。
2、继承性,某个节点被取消,其对应的子树也会全部被取消。
3、多样性,节点存在不同的实现,故每个节点会附带不同的功能。

Context的孩子们

在源码里实现只有4种实现,要弄懂context的源码其实把4种对应的实现学习一下就行,他们分别是:
emptyCtx: 一个空的ctx,一般用于做根节点
cancelCtx:核心,用来处理相关的取消操作。
timeCtx:用来处理超时相关操作。
valueCtx:附加值的实现方法。

cancelCtx

结构
在这里插入图片描述
done:用于判断是否完成
cancel:用于取消节点
err:取消时的错误,超时或主动取消

cancelCtx的取消实现
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)
	} else {
		close(d)
	}
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}

整个过程可以总结为:
1、前置判断,看是否为异常情况
2、关闭c.done,这样外部调用cancelCtx.Done()就会有返回结果。
3、递归调用子节点的cancel方法
4、视情况从父节点中移除子节点

总结

cancelCtx作用其实就两个:
1、绑定父子节点,同步取消信号,父节点取消子节点也会跟着取消
2、提供主动取消函数

Context安全传递数据

context包我们就用来做两件事:
1、安全传递数据
2、控制链路
安全传递数据,是指在请求中执行上下文线程安全的传递数据,依赖于WithValue方法

Context父子关系

特点:context的实例之间存在父子关系
1、当父亲取消或者超时,所有派生的子Context都会被取消或者超时。
2、当找key的时候,子Context先看自己有没有,没有则取祖先里找。

func TestParentValueContext(t *testing.T) {
	ctx := context.Background()
	childCtx := context.WithValue(ctx, "key1", 123)
	ccCtx := context.WithValue(childCtx, "key2", 124)
	v := childCtx.Value("key2")
	fmt.Println(v)
	v = ccCtx.Value("key1")
	fmt.Println(v)
}

输出结果:
在这里插入图片描述
控制是自上而下的,查找是自下而上的。

Context控制超时

最经典的用法是利用context控制超时。
控制超时,相当于我们监听两个channel,一个业务正常结束的channel,还有一个Done() 返回的。

func TestContextBusinessTimeOut(t *testing.T) {
	ctx := context.Background()
	timeoutCtx, cancel := context.WithTimeout(ctx, time.Second)
	defer cancel()
	end := make(chan struct{}, 1)
	go func() {
		MyBusiness()
		end <- struct{}{}
	}()
	ch := timeoutCtx.Done()
	select {
	// 超时分支
	case <-ch:
		fmt.Println("time out")
	// 正常业务分支
	case <-end:
		fmt.Println("business end")
	}
}

func MyBusiness() {
	time.Sleep(500 * time.Millisecond)
	fmt.Println("hello world")
}

运行结果:
在这里插入图片描述

Context包使用注意事项

1、一般只用作方法参数,而且是作为第一个参数。
2、所有公共方法,除非是util,helper之类的方法,否则都加上Context参数
3、不要用作结构体字段,除非你的结构体本身传达的也是一个上下文的概念。

Context包–面试要点

1、context.Context使用场景:上下文传递超时控制
2、context.Context原理:
父亲如何控制儿子:通过儿子主动加入到父亲的children里面,父亲只需遍历就可以。
3、valueCtx和timeCtx的原理:

总结

Context的主要功能就是用于控制协程退出和附加链路信息。核心实现的结构体有4个,最复杂的是cancelCtx,最常用的是cancelCtx和valueCtx。整天呈树状结构,父子节点间同步取消信号。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值