您的位置 首页 golang

Golang 带有取消功能的Context

本文基于golang 1.17对Golang 带有取消功能的Context的实现进行学习,了解其实现取消操作的实现。

开始之前我们先上一段简单的代码来看看效果。

 package main

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

func main() {
  ctx := context.TODO()
  cancel, cancelFunc := context.WithCancel(ctx)
  go func(ctx context.Context) {
    for {
      select {
      case <-ctx.Done():
        fmt.Println(ctx.Err())
        fmt.Println("end")
        return
      }
    }
  }(cancel)
  cancelFunc()
  time.Sleep(time.Second * 3)
}  

这段代码做了以下几件事情

  1. 创建一个没有具体实现的context context.TODO()函数返回没有具体实现的context,第10行
  2. 创建出带有取消功能的context,第11行
  3. 创建一个goroutines,并监听context是否被取消,第12~21 行
  4. 取消context,第22行
  5. 休眠3秒,第23行

最后的输出应该是:

context canceled

end

接下来我们来看看这个功能是怎么实现的。通过的上面的代码我们知道取消context这个动作是由函数cancelFunc()触发的,进入context.WithCancel()看看这个函数的具体实现

 // WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
  if parent == nil {
    panic("cannot create context from nil parent")
  }
  c := newCancelCtx(parent)
  propagateCancel(parent, &c)
  return &c, func() { c.cancel(true, Canceled) }
}  

函数将传入的context作为父级创建了一个新的子级context,然后返回新创建的context和一个CancelFun类型的函数,通过最开始的演示代码知道就是这个函数出发了取消的context的动作。在查看取消函数之前我们先看看新创建出的context的结构是什么

 // 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     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
}  

知道了cancelCtl的包含哪些字段之后进一步看看是如何取消context的

 // 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)
  }
}  

这是具体的取消函数,这个函数的主要作用就是向done中放入一个空的结构体,关闭chan d 第17行,然后依次取消当前context的子context,

接下来我们来看看context是怎么通过Done()得到的取消通知

 func (c *cancelCtx) Done() <-chan struct{} {
  d := c.done.Load()
  if d != nil {
    return d.(chan struct{})
  }
  c.mu.Lock()
  defer c.mu.Unlock()
  d = c.done.Load()
  if d == nil {
    d = make(chan struct{})
    c.done.Store(d)
  }
  return d.(chan struct{})
}  

通过阅读代码这个函数只是到done中的取值,如果没有就创建一个chan struct {},并加载到done。所有chan struct的作用只是一个占位的作用,当done中的chan 被关闭的的时候我们自然就可以知道context已经被取消。

在取消函数里我们看见

  d, _ := c.done.Load().(chan struct{})
  if d == nil {
    c.done.Store(closedchan)
  } else {
    close(d)
  }  

这段代码,closedchan 是一个已经关闭的chan

 var closedchan = make(chan struct{})

func init() {
  close(closedchan)
}  

所以如果在执行取消函数的时候done里没有值的话就直接加载一个已经关闭的chan。

带有取消功能context可以让我们控制创建的goroutines结束自己。

欢迎指正文章中的错误

文章来源:智云一二三科技

文章标题:Golang 带有取消功能的Context

文章地址:https://www.zhihuclub.com/98997.shtml

关于作者: 智云科技

热门文章

网站地图