阅读 818

Golang Context的学习笔记

Golang中的context有什么用?

context是golang标准库中的一个包,它定义了一个上下文类型,这个上下文类型可以在线程间,携带信号、值、超时时间等,起到识别和跟踪go中的每个goroutine的作用,进而达到控制它们的目的。比如我们可以对某个goroutine设置一个超时时间,然后就可以实现对该goroutine下所有衍生的子goroutine达到级联取消的作用。

context的底层数据结构

context首先是被定义成了一个接口,这个接口包含了4个方法:

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}
复制代码
  • Deadline() 可以获取到当前context是否被设置了过期时间,如果设置了,可以获取到它被设置的过期时间是什么
  • Done() 返回一个只可以读的chan, 当当前的context被取消或者到了Deadline,则会返回一个被关闭的channel
  • Err() 在Done() 被关闭后,返回一个context被关闭的原因
  • Value(key interface{}) 可以获取到key对应的value

另外context包中还有一个canceler接口:

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}
复制代码

实现了canceler的context是可以被直接取消的,然后在context包中,有emptyCtx,cancelCtx和timerCtx都对这个接口进行了实现。 虽然emptyCtx对context接口进行了实现,但是它是一个空实现,在我们实际使用的时候,相当于是一个root的context

接着是cancelCtx的结构体:

   342  // A cancelCtx can be canceled. When canceled, it also cancels any children
   343  // that implement canceler.
   344  type cancelCtx struct {
   345  	Context
   346  
   347  	mu       sync.Mutex            // protects following fields
   348  	done     chan struct{}         // created lazily, closed by first cancel call
   349  	children map[canceler]struct{} // set to nil by the first cancel call
   350  	err      error                 // set to non-nil by the first cancel call
   351  }
复制代码

cancelCtx组合了Context,并且它还实现了canceler接口,说明这是一个可以被取消的context。

接着我们看下propagateCancel的具体实现:

   232  func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
   233  	if parent == nil {
   234  		panic("cannot create context from nil parent")
   235  	}
   236  	c := newCancelCtx(parent)
   237  	propagateCancel(parent, &c)
   238  	return &c, func() { c.cancel(true, Canceled) }
   239  }
   ……
   ……
   249  // propagateCancel arranges for child to be canceled when parent is.
   250  func propagateCancel(parent Context, child canceler) {
   251  	done := parent.Done()
                //根的context永远不会被取消, 这个根context一般是基于context.Backgroud()或者是context.Todo()生成出来的
   252  	if done == nil {
   253  		return // parent is never canceled
   254  	}
   255      
                // 这段select好像旧的版本里面是没有的, 当发现父context已经被取消了, 则直接调用cancel取消父context下的所有子context
   256  	select {
   257  	case <-done:
   258  		// parent is already canceled
   259  		child.cancel(false, parent.Err())
   260  		return
                // 注意这个default, 当select中的case没有收到信号的时候, 会直接执行default, 所以这个select并不会阻塞整个方法的执行
   261  	default:
   262  	}
   263      
                // 查找父context的*cancelCtx 
   264  	if p, ok := parentCancelCtx(parent); ok {
   265  		p.mu.Lock()
                        // 找到了并且已经取消的话, 则取消所有的子context
   266  		if p.err != nil {
   267  			// parent has already been canceled
   268  			child.cancel(false, p.err)
   269  		} else {
                                // 初始化一个子context的map, 用于挂载子的context
   270  			if p.children == nil {
   271  				p.children = make(map[canceler]struct{})
   272  			}
   273  			p.children[child] = struct{}{}
   274  		}
   275  		p.mu.Unlock()
   276  	} else {    // 如果没有找到
   277  		atomic.AddInt32(&goroutines, +1)
                        // 启动一个goroutine监听父context的取消信号
   278  		go func() {
   279  			select {
                                // 如果父context取消, 则级联取消它下面挂的所有子的context
   280  			case <-parent.Done():
   281  				child.cancel(false, parent.Err())
                                // 这个case的主要的目的是: 
                                // 假如父的context一直不取消的话,那么当子的context取消后, 
                                // 这个监听的goroutine没有存在的必要,相对于这个goroutine泄露了,除非父的context取消。
                                // 为什么没有存在的必要呢?
                                // 因为子的context已经取消,跟父的context也不存在任何关系了,
                                // 父context取消,会调用cancel去取消所有的子context,
                                // 没有必要单独起保留这个goroutine对信号进行监听
   282  			case <-child.Done():
   283  			}
   284  		}()
   285  	}
   286  }
复制代码

propagateCancel的作用是为生成的子context安排一个可以挂靠的父context,这样当某个父context取消的时候,才能实现级联式的取消它下面所有的子context。具体的说明写在代码注释里面了。

然后是cancel的实现:

   392  // cancel closes c.done, cancels each of c's children, and, if
   393  // removeFromParent is true, removes c from its parent's children.
   394  func (c *cancelCtx) cancel(removeFromParent bool, err error) {
   395  	if err == nil {
   396  		panic("context: internal error: missing cancel error")
   397  	}
   398  	c.mu.Lock()
   399  	if c.err != nil {
   400  		c.mu.Unlock()
   401  		return // already canceled
   402  	}
   403  	c.err = err
   404  	if c.done == nil {
   405  		c.done = closedchan
   406  	} else {
   407  		close(c.done)
   408  	}
   409  	for child := range c.children {
   410  		// NOTE: acquiring the child's lock while holding parent's lock.
   411  		child.cancel(false, err)
   412  	}
   413  	c.children = nil
   414  	c.mu.Unlock()
   415  
   416  	if removeFromParent {
   417  		removeChild(c.Context, c)
   418  	}
   419  }
复制代码

其中最重要的就是removeFromParent参数,我们看在WithCancel方法中调用cancel时候传的是true,其他时候传都是false。这个参数控制着WithCancel是否被调用,所以我们可以看下WithCancel具体做了哪些事情。

   315  // removeChild removes a context from its parent.
   316  func removeChild(parent Context, child canceler) {
   317  	p, ok := parentCancelCtx(parent)
   318  	if !ok {
   319  		return
   320  	}
   321  	p.mu.Lock()
   322  	if p.children != nil {
   323  		delete(p.children, child)
   324  	}
   325  	p.mu.Unlock()
   326  }
复制代码

可以看到,这个方案其实就是从父的context中所有的子context移除了自己。那这个removeFromParent想要说明的是什么呢?其实我的理解就是:如果我们取消的是当前context,那么我们理所当然的应该将当前的context从它的父context的children集合中移除,但是我们移除当前context的子context的时候就不需要这么做了,因为我们在移除当前context的子context的时候,直接让context.children = nil就可以了

timerCtx的结构体的实现其实也是基于cancelCtx, 只不过它是新增了一个timer,通过定时器定时的去cancel,进而实现goroutine的定时取消。

context超时控制使用的基本姿势

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    s, err := GetData(ctx)
    if err != nil {
        fmt.Printf("error: %+v, s: %s \n", err, s)
        return
    }
    fmt.Printf("s: %+v \n", s)
}

func GetData(ctx context.Context) (string, error) {
    c := make(chan error)
    s := ""
    go func() {
        time.Sleep(1 * time.Second)
        s = "test"
        c <- nil
    }()
    select {
    case <-ctx.Done():
        return "error", ctx.Err()
    case <-c:
        return s, nil
    }
}
复制代码

如上, 在GetData方法中,我们去获取数据的时候,可以启动了一个goroutine去获取数据,比如发起一个HTTP的请求什么的,当超过2s没有数据返回的话呢,select中的ctx.Done() 就会受到一个信号,从而返回一个错误。在上面我的go func里面只是sleep了1s的时间,在实际的项目中,我们还需要注意,这个goroutine是否会泄露的问题,比如,如果超时了之后,GetData我们是直接返回了错误,但是这个go func这个goroutine什么时候退出我们一定要自己心里有数。

引用

Go Concurrency Patterns: Context

zhuanlan.zhihu.com/p/68792989

golang.org/src/context…

文章分类
后端
文章标签