阅读 626

golang的context的理解

golang的context的理解

文章美化版

golang的context的理解

context介绍

在Go服务器中,每个传入请求都在其自己的goroutine中进行处理。 请求处理程序通常会启动其他goroutine来访问后端,例如数据库和RPC服务。 处理请求的goroutine集合通常需要访问特定于请求的值,例如最终用户的身份,授权令牌和请求的期限。 当一个请求被取消或超时时,处理该请求的所有goroutine应该迅速退出,以便系统可以回收他们正在使用的任何资源。

GO的官方就开发了 context 包,可以轻松地跨API边界将请求范围的值,取消信号和截止日期传递给处理请求的所有goroutine。 该软件包可作为上下文公开使用。

context的解决问题的场景

场景一

avatar

使用conext来解决一下,当请求结束后,立马停止正在做的事情

func main() {
	http.HandleFunc("/hello", func(writer http.ResponseWriter, request *http.Request) {
		log.Println("我接收到了请求")
		var wg sync.WaitGroup
		wg.Add(1)
		// 做 A 事情
		go func() {
			defer wg.Done()
			select {
			case <-request.Context().Done(): // 用来判断 这次请求是是否结束了
				// do something
				log.Println("我 很快乐的结束了")
			}
		}()
		// 做B事情
		// 做C事情
		wg.Wait()
	})

	fmt.Println(http.ListenAndServe(":8080", nil))
}
复制代码

使用context 用来解决 goroutine 之间退出通知、元数据传递的功能(同步请求特定数据、取消信号以及处理请求的截止日期)

context的怎么使用

使用context来 通知子协成

// 使用context来 通知子协成结束
func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()
	// 500ms
	go handle3(ctx, 3*time.Second)
	// 如果 select中的 任何一个case都没有满足的 则会指定等待下去直到有符合条件的case
	select {
	case <-ctx.Done():
		// main context deadline exceeded 代表 是由于超过了过期 时间的导致的错误
		fmt.Println("main", ctx.Err())
	}
	// 避免程序,过早退出导致 子协成的 没有打印出来
	time.Sleep(time.Second * 1)
}

// 使用context 同步信号
func handle3(ctx context.Context, duration time.Duration) {
	select {
	case <-ctx.Done():
		fmt.Println("handle", ctx.Err())
	case <-time.After(duration):
		fmt.Println("process request with", duration)
	}
}
复制代码

使用context 来向 子协成传递数据(key一定是可以比较的)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	valueCtx := context.WithValue(ctx, "traceId", "abc123-abc")

	go func(ctx context.Context) {
		select {
		case <-ctx.Done():
			fmt.Println(ctx.Value("traceId"))
		}
	}(valueCtx)

	time.Sleep(1 * time.Second)
	cancel()
	time.Sleep(1 * time.Second)
}
复制代码

context包中的 一些重要数据结构

context接口

type Context interface {
    // 返回 设置的截止日期和 是否 设置deadline
    Deadline() (deadline time.Time, ok bool)
    
    // 当context 被取消了 或者到了 deadline 时间会返回一个 只读chan
    Done() <-chan struct{}
    
    // 返回被 取消的原因 
    Err() error
    
    // 返回context中的值,它会递归去根据key拿取数据
    Value(key interface{}) interface{}
}

复制代码

context对外提供的主要方法

// 用来做为 contex的父context  是一个 empty的Context
func Background() Context {}

// 当你还不清楚 用那种context合适的时候,使用他作为一个占位
func TODO() Context {}

// 创建一个可以 cancel context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {}

// 创建一个带有截止日期的 context
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {}

// WithTimeout 底层就是调用 WithDeadline 创建一个有超时时间的context
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {}

// 创建一个存储 k-v 对的 context
func WithValue(parent Context, key, val interface{}) Context {}
复制代码

cancelCtx

type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     chan struct{}         // 这个类似 一个信号量,用来通知context 需要取消执行了
	children map[canceler]struct{} // 用来存放 子context
	err      error                 // set to non-nil by the first cancel call
}
复制代码

疑问解答

context是如何通知子协成结束的?

  • 这里以WitchCancel()的举例

    1、构建context,并将子context添加到 children map[canceler]struct{}

    2、当cancel触发 调用时候,遍历 当前的context的 children,递归的 去cancel,并将 子context的 done close这步至关重要(类似一个信号量)

    3、删除当前节点从父context中

  • context树

avatar


// 重点
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
	// 一旦 c.done close后,对应的context就会 收到信号,根据收到的信号,做其他(提前结束业务流程)操作
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done)
	}
	// 循环递归的形式调用 children的去 cancel子context
	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()

	// 移出自己从父context中
	if removeFromParent {
		removeChild(c.Context, c)
	}
}
复制代码

为什么对外部返回一个cancel呢?

当程序出现panic或者 满足了一定条件 我们可以更灵活去 defer cancel() 或者 cancel() 和通知子context 退出程序,通过使用 defer cancel()避免了 当程序出现了 panic,而无法 取消 子context,从而导致内存泄露(用的是WitchCancel的Context)

canel传true和false的用意和区别?

  • 对外提供了 cancel,外部自己触发 为true 或者 因为到了截止时间传递ture(符合了取消条件或明确了要取消,则从父context中移出自己)

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	return &c, func() { c.cancel(true, Canceled) }
}

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	// d小于当前时间了,说明 已经 deadline
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(false, Canceled) }
	}
	if c.err == nil {
		// 启动了一个定时器,dur后就会执行了 cancel
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}
复制代码
  • cancel的 取消 子context逻辑,在取消children中 传递了false
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
	// 一旦 c.done close后,对应的context就会 收到信号,根据收到的信号,做其他(提前结束业务流程)操作
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done)
	}
	for child := range c.children {
		// 注意这里 传了false
		child.cancel(false, err)
	}
	// 上面取消完毕后,将 该context中的 children中置为nil
	c.children = nil
	c.mu.Unlock()

	// 当外部调用了 或者 因为到了 截止日期 会传入true 将自己从 父context中取消自己
	if removeFromParent {
		removeChild(c.Context, c)
	}
}
复制代码
  • 看图分析

avatar

  • 当 child.cancel(true), err)

    1、ctx11 调用cancel(true,err)

    2、遍历 ctx11的 children,调用child.cancel(true, err)

    3、以递归的形式取消 children, g1,g2,g3,关闭自己的done 将自己的 chidren=nil

    4、删除自己从 父cotext中removeChild(c.Context, c) 也就是 将 自己(g1,g2,g3)从父(ctx11)context 中 删除

    5、children遍历完毕 ,将 ctx11.children =nil, 然后将 ctx1删除从父context中

  • 当 child.cancel(false, err)

    1、ctx11 调用cancel(true,err)

    2、遍历 ctx11的 children,调用child.cancel(false, err)

    3、以递归的形式取消 children, g1,g2,g3,关闭自己的done 将自己的 chidren=nil

    4、children遍历完毕 ,将 ctx11.children =nil(完全可以将 g1,g2,g3从children中删除), 然后将 ctx1删除从父context中

  • 总结

    1、child.cancel(true), err)child.cancel(false), err) 每次都多执行一步,removeChild(c.Context, c),而c.children =nil 完全可以将 子context(g1,g2,g3)从 children中删除

    2、只有外部调用 cancel 或者 截止日期到了 cancel才会传true cancel(true), err), 遍历children,传递cancel信号,并删除自己从 父context中

如何判断协成超时呢?

  • WithDeadline的做法
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		// 在这里会 创建并启动一个定时器,dur时间到了后,就会执行AfterFunc 函数,从而 `c.cancel(true, DeadlineExceeded)`
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}
复制代码
  • 验证 AfterFunc是创建并启动了一个定时器了
// 2s过后,打印hello
func main() {
	go func() {
		// 当你创建 time.AfterFunc 定时器就时候就开始执行了
		_ = time.AfterFunc(time.Second*2, func() {
			fmt.Println("hello")
		})
	}()
	time.Sleep(5 * time.Second)
}
复制代码

如何添加 子context到 父亲的context的 children

func propagateCancel(parent Context, child canceler) {
	// 当调用 Done是 会对Done做初始化,(顶级Context除外,因为他是一个空的实现)
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err())
		return
	default:
	}

	// 得到一个 cancelCtx的,因为只有 cancelCtx 结构体才有 children
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		// err 不为nil,则说明 父context 已经取消了
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err)
		} else {
			// 构造 children map并将 child 挂载到 父context上
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		atomic.AddInt32(&goroutines, +1)
		// case1 去掉 可能导致 父节点的取消信号 永远传递不到 子节点
		// case2 去掉 如果父节点一致不取消,那么就会导致这个goroutine 泄露
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
复制代码

timeCtx的cancel如何取消的?


func (c *timerCtx) cancel(removeFromParent bool, err error) {
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	
	/*
	c := &timerCtx{
	cancelCtx: newCancelCtx(parent),
	deadline:  d,
	}
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(false, Canceled) }
	}
	*/
	// 上面的情况 就可能导致 timer为nil,所以需要判断 c.timer !=nil
	// cancel的调用 可能是 外部直接调用,此时timer 还没有触发执行,也有可能是 deadline后,timer触发调用的
	if c.timer != nil {
		// 这里 要停止 定时器,外部已经cancel过了,避免因为到了 deadline后,timer触发再次调动 cancle
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}
复制代码

parentCancelCtx(parent) 如何才能返回 false???

参考链接

深度解密Go语言之context

文章分类
后端
文章标签