示例
context包也算是golang日常经常用到的包,对于context包的定义是在不同程序之间设置截止时间、取消等信号或者其他请求相关值传递。
func main(){
// example 1
ctx,cancel := context.WithTimeout(context.Background(),1*time.Second)
go doSomething(ctx)
time.Sleep(2*time.Second)
cancel()
fmt.Println("example 1 closed")
ctx2,cancel2 := context.WithTimeout(context.Background(),1*time.Second)
go doSomething(ctx2)
cancel2()
time.Sleep(1*time.Second)
fmt.Println("example 2 closed")
}
func doSomething(ctx context.Context){
select {
case <-ctx.Done():
fmt.Println("context done",ctx.Err())
}
}
输出:
context done context deadline exceeded
example 1 closed
context done context canceled
example 2 closed
示例解析
首先两个例子都设置了超时信号,对应也返回了cancel函数,对于例子1,由于超时信号只有1秒,但是sleep了2秒,所以返回的结果为deadline exceeded,符合预期。
对于例子2,由于设置了1秒超时的上下文之后,立即调用了对应的cancel函数,所以返回了context canceled的结果,预期也是符合的。
源码阅读
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
上述为context包超时的调用,可以看到只是内部调用WithDeadline的函数,没有什么实质性内容,只是将time.Duration作为了time.Time的转换。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
对于WithDeadline,由于context可以完成嵌套的调用(在函数参数已经传入一个Context),所以在父级context本身设置了过期时间,并且时间先于当前的d时间参数的情况下,直接返回WithCacel(后面说明)。如果不满足前面提到的条件,即创建一个context,绑定父子关系(后面说明),顺便看下时间是否已经到达,到达则直接调用取消函数(后面说明),否则创建一个定时器,到期调用取消函数。
下面先来看WithCacel的调用以及对应的取消函数,再去看绑定父子关系的propagateCancel函数调用。
// 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) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
withCacel函数本身就是创建一个cancel的context,其实内部包了一层传入的context参数,关联父子上下文之间的关系,具体可以看下文。
// 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():
}
}()
}
}
propagateCancel函数主要作用如下:
- 当父级的Done函数为nil的时候直接返回;
- 检测父级是否是可以取消的context,如果是会判断是否已经取消,已取消则子context也取消,否则child加入parent的列表中;
- 其他情况下,会启动一个goroutine来监听parent和child的Done channel,如果父级done则调用child的取消函数
最后再看看cacel函数的调用:
// 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
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
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)
}
}
cancel函数的调用很简单,只是将对应的err写入context的err属性,关闭对应的done channel,如果有child列表,则一一调用cancel函数关闭对应的资源。
简要总结
context主要的作用其实是同步信号,由于goroutine创建的树结构的关系,当父节点(指上面的goroutine)关闭了之后,子routine还暂用对应的运算资源,没有同步对应的信号。