Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > golang Context超时

详解golang中Context超时控制与原理

作者:m旧裤子

Context本身的含义是上下文,我们可以理解为它内部携带了超时信息、退出信号,以及其他一些上下文相关的值,本文给大家详细介绍了golang中Context超时控制与原理,文中有相关的代码示例供大家参考,需要的朋友可以参考下

Context

在Go语言圈子中流行着一句话:

Never start a goroutine without knowing how it will stop。

翻译:如果你不知道协程如何退出,就不要使用它。

在创建协程时,我们可能还会再创建一些别的子协程,那么这些协程的退出就成了问题。在Go1.7之后,Go官方引入了Context来实现协程的退出。不仅如此,Context还提供了跨协程、甚至是跨服务的退出管理。

Context本身的含义是上下文,我们可以理解为它内部携带了超时信息、退出信号,以及其他一些上下文相关的值(例如携带本次请求中上下游的唯一标识trace_id)。由于Context携带了上下文信息,父子协程之间就可以”联动“ 了。

Context标准库

在Context标准库中重要的结构 context.Context其实是一个接口,它提供了Deadline、Done、Err、Value这4种方法:

type Context interface {
   Deadline() (deadline time.Time, ok bool)
   Done() <-chan struct{}
   Err() error
   Value(key interface{}) interface{}
 }

Context是一个接口,这意味着需要有对应的具体实现。用户可以自己实现Context接口,并严格遵守Context接口。

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
}

因此,要具体使用Context,需要派生出新的Context。我们使用的最多的还是Go标准库中的实现。
前三个函数都用于派生出有退出功能的Context。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

Context实践

eg:

下面的代码中childCtx是preCtx的子Context,其设置的超时时间为300ms。但是preCtx的超时时间为100ms,因此父Context退出后,子Context会立即退出,实际的等待时间只有100ms。

func main() {
   ctx := context.Background()
   before := time.Now()
   preCtx, _ := context.WithTimeout(ctx, 100*time.Millisecond)
   
   go func() {
   childCtx, _ := context.WithTimeout(preCtx, 300*time.Millisecond)
   select {
    case <-childCtx.Done():
   after := time.Now()
   fmt.Println("child during:", after.Sub(before).Milliseconds())
   }
 }()
 
 select {
    case <-preCtx.Done():
    after := time.Now()
    fmt.Println("pre during:", after.Sub(before).Milliseconds())
 }
 }

这是输出如下,父Context与子Context退出的时间差接近100ms:

pre during: 104
child during: 104

当我们把preCtx的超时时间修改为500ms时:

preCtx ,_:= context.WithTimeout(ctx,500*time.Millisecond)

从新的输出中可以看出,子协程的退出不会影响父协程的退出。

child during: 304
pre during: 500

Context底层原理

Context在很大程度上利用了通道的一个特性:通道在close时,会通知所有监听它的协程。

每个派生出的子Context都会创建一个新的退出通道,这样,只要组织好Context之间的关系,就可以实现继承链上退出信号的传递。如图所示的三个协程中,关闭通道A会连带关闭调用链上的通道B,通道B会关闭通道C。

要使用context的退出功能,需要调用WithCancel或WithTimeout,派生出一个新的结构Context。WithCancel底层对应的结构为cancelCtx,WithTimeout底层对应的结构为timerCtx,timerCtx包装了cancelCtx,并存储了超时时间。

type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of 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
	cause    error                 // set to non-nil by the first cancel call
}

type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

cancelCtx第一个字段保留了父Context的信息。children字段则保存了当前Context派生的子Context的信息,每个Context都会有一个单独的done通道。

而WithDeadline函数会先判断父Context设置的超时时间是否比当前Context的超时时间短,如果是,那么子协程会随着父Context的退出而退出,没有必要再设置定时器。

当我们使用了标准库中默认的Context实现时,propagateCancel函数将子Context加入父协程的children哈希表中,并开启一个定时器。当定时器到期时,会调用cancel方法关闭通道,级联关闭当前Context派生的子Context,并取消与父Context的绑定关系。这种特性就产生了调用链上连锁的退出反应。

以上就是详解golang中Context超时控制与原理的详细内容,更多关于golang Context超时的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文