golang-context

Posted inet_ygssoftware

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang-context相关的知识,希望对你有一定的参考价值。

标题golang-context

1. context 常用方法,以及各种适用于什么场景

1.1 context含有的方法

	var ctx context.Context
	var cancel context.CancelFunc
	// 1,传递key,value的值
	ctx = context.WithValue(context.Background(), "key", "value")
	// 2,超时控制timeout
	ctx, cancel = context.WithTimeout(context.Background(), time.Second*10)
	// 3,超时控制,deadline
	ctx, cancel = context.WithDeadline(context.Background(), time.Now().Add(time.Second*10))
	// 4,取消控制
	ctx, cancel = context.WithCancel(context.Background())
	// 5,养成好习惯,defer触发cancel函数;ps: cancel函数幂等
	defer cancel()

1.2 方法适用场景和伪代码示例

1.2.1 值传递:比如gin框架中用来传递key,value的值,自己简单示例如下
func readContext(ctx context.Context) {
	traceId, ok := ctx.Value("key").(string)
	if ok {
		fmt.Printf("readContext key=%s\\n", traceId)
	} else {
		fmt.Printf("readContext no key\\n")
	}
}

func main() {
	ctx := context.Background()
	readContext(ctx) //output: readContext no key
	ctx = context.WithValue(ctx, "key", "beautiful")
	readContext(ctx) //output: readContext key=beautiful

}

func TestWithValueContext(t *testing.T) {
	main()
}
1.2.2 超时控制-timeout: http请求设置超时时间
func httpRequest(ctx context.Context) {
	for {
		// process http requests
		select {
		case <-ctx.Done():
			fmt.Println("http requests cancel")
			return
		case <-time.After(time.Second * 1):
		}
	}

}

func TestTimeoutContext(t *testing.T) {
	fmt.Println("start TestTimeoutContext")
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
	defer cancel()
	httpRequest(ctx)
	time.Sleep(time.Second * 5)
}
1.2.3, 超时控制-deadline: 比如文件io或者网络io等耗时操作,可以查看剩余的时间是否充足,决定是否进行下一步操作
func copyFile(ctx context.Context)  {
	deadline, ok := ctx.Deadline()
	if ok == false {
		return
	}
	isEnough := deadline.Sub(time.Now()) > time.Second*5
	if isEnough {
		// write content to file
		fmt.Println("copy file")
	} else {
		fmt.Println("isEnough is false return")
		return
	}
}

func TestDeadlineContext(t *testing.T) {
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second*4))
	defer cancel()
	copyFile(ctx)
	time.Sleep(time.Second * 5)
}
1.2.4. 取消控制: goroutine发送取消信号,保证自己这个逻辑中发散出去的goroutine全部成功取消
func gen(ctx context.Context) <-chan int {
	ch := make(chan int)
	go func() {
		var n int
		for {
			select {
			case ch <- n:
				n++
				time.Sleep(time.Second)
			case <-ctx.Done():
				return
			}
		}
	}()
	return ch
}

func TestCancelContext(t *testing.T) {
	// 创建一个Cancel context
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
			// 达到要求后触发 cancel
			cancel()
			break
		}
	}

}

2. context包的底层实现是什么样的?

2.1 key,value 传值底层实现

  • 函数底层实现代码(golang v1.16),其核心就是当本context无法获取到key的值的时候,递归父context获取

    type valueCtx struct {
    Context // 保存着本节点的父节点
    key, val interface{} // 保存着本节点的key,value值
    }

    // 新建一个value context的实现
    func WithValue(parent Context, key, val interface{}) Context {
    if parent == nil {
    panic(“cannot create context from nil parent”)
    }
    if key == nil {
    panic(“nil key”)
    }
    if !reflectlite.TypeOf(key).Comparable() {
    panic(“key is not comparable”)
    }
    return &valueCtx{parent, key, val}
    }

    func (c *valueCtx) Value(key interface{}) interface{} {
    // 如果key,value在本节点就有,那么直接取值
    if c.key == key {
    return c.val
    }
    // 否则递归向父节点获取
    return c.Context.Value(key)
    }

2.2 cancel实现

2.2.1 cancelCtx 结构体
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
// 触发取消以后会递归取消所有子节点
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     chan struct{}         // created lazily, closed by first cancel call
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
}
2.2.2 cancelCtx实现了cancel函数,逻辑如下
  • 1, 锁保证并发冲突,避免并发冲突

  • 2,关闭c.done这个channel,通过这个传递信号(往后细化分析)

  • 3,遍历关闭所有子节点,保证不会内存泄漏

  • 4,释放自己的所有子节点后,将自己的子节点map赋值为nil

  • 5,将自己从自己的父节点中进行移除,这个只有在调用WithCancel()方法的时候会触发,也就是说传入参数removeFromParent为true(往后细化分析)

    func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
    panic(“context: internal error: missing cancel error”)
    }
    c.mu.Lock() // 1,锁保证并发冲突
    if c.err != nil {
    c.mu.Unlock()
    return // already canceled
    }
    c.err = err
    if c.done == nil {
    c.done = closedchan
    } else {
    close(c.done) // 2,关闭done这个channel
    }
    // 3,遍历所有子节点,然后调用cancel函数
    for child := range c.children {
    // NOTE: acquiring the child’s lock while holding parent’s lock.
    child.cancel(false, err)
    }
    // 4,释放自己的所有子节点后,将自己的子节点map赋值为nil
    c.children = nil
    c.mu.Unlock()

      // 5,将自己从自己的父节点中进行移除,这个只有在调用withCancel的时候会传入true
      if removeFromParent {
      	removeChild(c.Context, c)
      }
    

    }

2.2.3 细化:c.done的信号传递

  • 这个是基于所有channel的特性,当监听一个channel,channel为空的时候会阻塞,但是如果channel被关闭,那么将不会阻塞,而会读取到一个空值

  • 基于上述特性,实现了关闭这个channel,而其他所有监听此channel的goroutine都收到此信号

  • 代码举例

    func Done(ch chan struct{}, count int) {
    for {
    select {
    case <-ch:
    fmt.Println(count)
    return
    }
    }
    }

    func TestCloseChannel(t *testing.T) {
    signalChannel := make(chan struct{})
    go Done(signalChannel, 1)
    go Done(signalChannel, 2)
    go Done(signalChannel, 3)
    time.Sleep(3)
    fmt.Println(“close signalChannel”)
    close(signalChannel)

      // 阻塞当前goroutine
      select {
    
      }
    

    }

2.2.4 细化:removeFromParent参数-是否从父节点delete自己
  • 先看下removeChild的实现代码

    // removeChild removes a context from its parent.
    func removeChild(parent Context, child canceler) {
    p, ok := parentCancelCtx(parent)
    if !ok {
    return
    }
    p.mu.Lock()
    if p.children != nil {
    delete(p.children, child)
    }
    p.mu.Unlock()
    }

  • 为什么调用WithCancel()的时候,也就是新建一个节点的时候会传入removeFromParent=true然后调用removeChild()呢?

    • 因为你调用cancel作用的更多的处理的挂靠在你这个context上的子节点,而只有最后一步才是真正的释放自己
    • 举例:
      • 1,第一步:假如你创建的一个cancelContext,挂靠在在根节点上(contextBackgroud)上,那你下面的子节点都会因为你的 c.children = nil 而释放。
      • 2,第二步:然后逻辑上你自己都调用了cancel,那么你自己也要释放了,所以就将自己从从父节点中delete的
  • 为什么其他删除子节点的时候不会调用?

    • 1,因为其中有一个操作是 delete(p.children, child) ,这个操作会删除父节点的子节点的map中的自己,而一边遍历和一边删除map是会出问题的
    • 2,同时由于cancel()函数中有操作为 c.children = nil ,所以也无需说去做这种操作

以上是关于golang-context的主要内容,如果未能解决你的问题,请参考以下文章

golang-context

golang-context

微信小程序代码片段

VSCode自定义代码片段——CSS选择器

谷歌浏览器调试jsp 引入代码片段,如何调试代码片段中的js

片段和活动之间的核心区别是啥?哪些代码可以写成片段?