golang库context学习

context库

context最早的背景说明还是来源于官方的 博客,说明如下:

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

再次背景之下,谷歌就开发了context的库,可以轻松地将跨API边界的请求范围的值,取消信号和截止日期传递给处理请求的所有goroutines。

context库的出现背景是为了解决一个goroutine退出,方便快速的让其他goroutine退出,从而提供资源的利用率。

源码

包上下文定义了上下文类型,其中包含截止日期,跨越API边界的取消信号和其他请求范围的值和进程之间。传入服务器的请求应创建一个上下文,然后传出对服务器的调用应接受上下文,功能链。它们之间的调用必须传播Context,可以选择替换它与使用WithCancel,WithDeadline,WithTimeout或WithValue。取消上下文后,所有
从中派生的上下文也会被取消。WithCancel,WithDeadline和WithTimeout函数采用上下文(父级)并返回派生的上下文(子级)和一个CancelFunc。调用CancelFunc取消子项及其子项子代,删除父代对子代的引用,然后停止任何关联的计时器。未能调用CancelFunc会泄漏子代及其子代,直到取消父代或定时器触发。审核工具检查所有对象上都使用了CancelFuncs控制流路径。使用上下文的程序应遵循以下规则来保留接口跨软件包保持一致,并启用静态分析工具来检查上下文传播:不要将Context存储在struct类型中;相反,传递一个上下文显式地提供给需要它的每个函数。上下文应该是第一个参数,通常命名为ctx:

 func DoSomething(ctx context.Context,arg Arg)error {
  ...使用ctx ...
}

即使函数允许,也不要传递nil Context。传递上下文如果不确定使用哪个上下文,也可以传递context.TODO。仅将上下文值用于传递过程和请求的请求范围的数据API,而不是用于将可选参数传递给函数的API。相同的上下文可以传递给在不同goroutine中运行的函数;上下文可以安全地被多个goroutine同时使用。

以上就是源码头部注释的说明,从说明也可看出整个包的具体使用思路与功能。

WithCancel使用示例
package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	// 开启一个协程运行
	go func() {
		for {
			select {
			case <- ctx.Done():
				fmt.Println("context done")
				return
			}
		}
	}()
	time.Sleep(2*time.Second)
	// 主协程取消
	cancel()
	fmt.Println("main cancel")
	// 为了让ctx.Done的协程能够打印出来context done
	time.Sleep(2*time.Second)
}

context done
main cancel

输出如下,从该示例中就可以看出ctx传入到另一个协程中,cancel在不同的协程中来取消,从而完成了一个协程退出的通知机制。看了这个功能之后,大家平常使用的方式可能是如下代码的方式;

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan struct{})
	go func(){
		for {
			select {
			case <- ch:
				fmt.Println("done")
				return
			}
		}
	}()
	time.Sleep(2*time.Second)
	fmt.Println("mian done")
	ch <- struct{}{}
	time.Sleep(2*time.Second)

}
mian done
done

运行结果如下,得出的效果跟用WithCancel效果相同,那我们就来查看一下WithCancel的源码是如何实现的。

//位于context.go文件
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
	if parent.Done() == nil {
		return // parent is never canceled
	}
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
			child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

从代码中可以看见WithCancel就是通过parent生成一个cancelCtx实例,然后将该实例加入到parent的孩子队列中,这样就建立了从parent到child的关系。

然后再函数返回的时候不仅返回了cancelCtx实例,还返回了一个cancel函数,该函数就是调用cancelCtx实例的cancel方法。我们继续查看cancel方法的流程。

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
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done)    // 关闭c.done的通道  这样就通知取消
	}
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)    // 调用所有子child的关闭函数关闭
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)  // 如果需要从父那边移除则移除该c
	}
}

从用了Lock的函数可知,ctx是线程安全的操作,而且context使用了树形的通知结构,所有添加的子节点都会执行取消的函数,并且取消之后会从父的ctx中移除该ctx。从这个函数也可以看一下整个cancelCtx的定义可知,所有的机制都是基于上述的思路,通过chan来通知不同协程中的监听事件。大致思路明白之后,我们再看看其他的用法。

WithDeadline使用示例
package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	d := time.Now().Add(50 * time.Millisecond)
	ctx, cancel := context.WithDeadline(context.Background(), d)
	
	defer cancel()

	select {
	case <-time.After(1 * time.Second):
		fmt.Println("overslept")
	case <-ctx.Done():
		fmt.Println(ctx.Err())
	}
}
context deadline exceeded

从输出可以看出,在五十毫秒之后关闭了ctx.Done()返回的chan,假如我们将五十毫秒调整到大于1秒,此时就会输出overslept。该功能可以让ctx在协程的传递过程中,设置超时机制。我们看一下具体的源码。

// 位于context.go
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {  // 检查是否大于parent的过期时间
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)                             // 大于当前时间则使用parent的过期时间
	}
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}                              // 生成一个timerCtx实例
	propagateCancel(parent, c)     // 加入到parent中
	dur := time.Until(d)       
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(false, Canceled) }   // 假如当前时间已经过了先执行取消,然后返回cancel函数提供程序手动调用
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() { 					// 添加一个过期时间函数,过了dur时间后调用c.cancel函数
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }     // 正常返回
}

从WithDeadline的实现过程来看,主要就是添加了time.AfterFunc函数,来设置一个过期回调函数来关闭通道。

总结

context包是一个线程安全,易于使用的库,从而提供了一种简便的方式来实现跨协程的通行机制,很多其他的包也都基于context包来实现对应的资源回收、消息通知等工作,现在应用比较广泛的就是我们大家查用的web框架中,大量的使用了context包来进行业务的处理,context的源码不多但是设计精巧很值得,很值得学习与借鉴。由于本人才疏学浅,如有错误请批评指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值