Golang Context 详细介绍

Golang context

  本文包含对context实现上的分析和使用方式,分析部分源码讲解比价多,可能会比较枯燥,读者可以直接跳过去阅读使用部分。

  ps: 作者本着开源分享的精神撰写本篇文章,如果出现任何误差务必留言指正,作者会在第一时间内修正,共同维护一个好的开源生态,谢谢!!!

一、简介

  作者所讲的context的包名称是: "golang.org/x/net/context" ,希望读者不要引用错误了。

  在godoc中对context的介绍如下:

 Package context defines the Context type, which carries deadlines, cancelation signals, and other request-scoped values across API boundaries and between processes.

 As of Go 1.7 this package is available in the standard library under the name context. https://golang.org/pkg/context. 

Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context. The chain of function calls between must propagate the Context, optionally replacing it with a modified copy created using WithDeadline, WithTimeout, WithCancel, or WithValue.

Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:

func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}

Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.

Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

See http://blog.golang.org/context for example code for a server that uses Contexts.
View Code

  鉴于作者英文水平有限,在这里不进行对照翻译,以免误导读者。它的第一句已经介绍了它的作用了:一个贯穿API的边界和进程之间的context 类型,可以携带deadlines、cancel signals和其他信息。就如同它的中文翻译一样:上下文。在一个应用服务中会并行运行很多的goroutines或进程, 它们彼此之间或者是从属关系、竞争关系、互斥关系,不同的goroutines和进程进行交互的时候需要进行状态的切换和数据的同步,而这就是context包要支持的功能。

二、解析

  context的接口定义如下:

   每一个接口都有详细的注释,这里就不重复了。 在context的源码中有以下几个结构体实现了Context Interface:

// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
   // Deadline returns the time when work done on behalf of this context
   // should be canceled.  Deadline returns ok==false when no deadline is
   // set.  Successive calls to Deadline return the same results.
   Deadline() (deadline time.Time, ok bool)
   // Done returns a channel that's closed when work done on behalf of this
   // context should be canceled.  Done may return nil if this context can
   // never be canceled.  Successive calls to Done return the same value.
   Done() <-chan struct{}
   // Err returns a non-nil error value after Done is closed.  Err returns
   // Canceled if the context was canceled or DeadlineExceeded if the
   // context's deadline passed.  No other values for Err are defined.
   // After Done is closed, successive calls to Err return the same value.
   Err() error
   // Value returns the value associated with this context for key, or nil
   // if no value is associated with key.  Successive calls to Value with
   // the same key returns the same result.
   Value(key interface{}) interface{}
}

2.1 empty context

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int

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
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

  这是一个空的ctx类型,每一个返回值都为空,它什么都功能都不具备,主要的作用是作为所有的context类型的起始点,context.Background()函数返回的就是这中类型的Context:

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
    return background
}

   empty context的作用作者下面会阐述。 

2.2 cancle 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     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
}

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

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.err
}

func (c *cancelCtx) String() string {
    return fmt.Sprintf("%v.WithCancel", c.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
    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)
    }
}

  cancelctx中的成员变量:

     `done chan struct{}`: 在调用Done()函数时会将该变量返回,这可以用在多goroutines之间进行状态同步; 

    `children map[canceler]struct{}`: 一个context可以被其他context 引用,被引用的context为parant,引用context为children,该变量包含所有的children context;

    `Context`:  关联了Context接口,实现了Done()和Err(),但是没有实现Value()和Deadline();

      cancel函数是一个受保护的函数,不能在外部进行调用。可以看到在执行这个函数的时候 done channel会被关闭掉,同时它会调用所有的children context的cancel函数,但是没有解除彼此的依赖关系。这实际上比较好理解,因为children context的生命周期是依赖与parant context的。同时它还要判断是否调用 removeChild(c.Context, c)函数将解除对parant context的引用关系。

   在context.WithCancel(parent Context) 函数中返回的就是cancelCtx;

   

2.3 timer context

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.
	deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
	return c.deadline, true
}

func (c *timerCtx) String() string {
	return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, time.Until(c.deadline))
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

   timer context 中的成员变量:

    `cancelCtx`: timerCtx 关联了 canelCtx类型;

    `timer`:  一个定时器,用来设置超时的时间;

    `dealine`: 一个时间类型,用来记录死亡时间;

  cancel函数 :1)它会先触发c.cancelCtx的cancel操作,但没有解除c.cancelCtx与parentCtx的依赖关系。2)判断是否去解除自身对于parentCtx的依赖, 3)停止它的timer,这个时候计时就结束了;

  它只实现了Deadline()函数,值得注意的是timeCtx和其包含的cancleCtx 是依赖于同一个parentCtx的。

  在WithDeadline(parent Context, deadline time.Time)和WithTimeout(parent Context, timeout time.Duration)函数中返回的就是 timerCtx;

2.4 value context  

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
	Context
	key, val interface{}
}

func (c *valueCtx) String() string {
	return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

func (c *valueCtx) Value(key interface{}) interface{} {
	if c.key == key {
		return c.val
	}
	return c.Context.Value(key)
}

   value context成员变量:

    Context:它关联了Context接口

    key,val interface{} :两个变量形成一个key-value结构;

   它只实现了Value()函数,可以返回key对应的value,这里需要注意的是,在查询value的时候,如果当前context中没有,会向上级的context进行搜索,递归查询

  WithValue(parent Context, key, val interface{}) 函数返回的即为value context。

2.5 总结

  综上四种类型的context 总结如下:

Name Deadline Done Err Value 继承
empty + + + + nil
cancel - + + - Context
timer + - - - canelCtx
value - - - + Context

  我们可以发现除了empty以外其他三种类型都没有完全去实现Context接口定义的所有函数,如果直接实例化一个cancelCtx对象,但是没有对 Context部分进行赋值,当调用其Value和Deadline函数会崩溃,timerCtx和valueCtx也是同样的道理。请读者一定要记住以上四种类型,这样你会很容易理解下面的内容。

 三、 Context的使用

3.1 Context 常用函数

   我们在上面的介绍过程中提到了很多函数:

//创建一个Cancel context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
//创建一个带有 deadline的Timer context func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
//创建一个带有超时的Timer context func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
//创建一个Value context func WithValue(parent Context, key, val interface{}) Context

   这些函数都是在使用Context中经常用到的,我们接下来说明它们的功能。

WithCancel

介绍:

  复制parentCtx,同时创建一个新的Done Channel返回Cancel函数,当以下两种情况发生时会关闭Done Channel,触发Done信号:

  1) 返回的Cancel函数被调用;

  2) parentCtx触发了Done信号;

  通常一个Context生命周期截止了(不再被需要的时候)就要立刻调用Cancel函数

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

   在newcancel函数中实例化一个cancel context对象:  

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

   propagateCancel如注释所述:向上找到最近的可以被依赖的父context,将子context放入 parent的children队列;如果找不到就开一个goroutines来等待主链退出的通知。

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

  parentCancelCtx : 寻找沿着parant引用向上追溯,直到发现一个cancelCtx;

// parentCancelCtx follows a chain of parent references until it finds a
// *cancelCtx. This function understands how each of the concrete types in this
// package represents its parent.
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
   for {
      switch c := parent.(type) {
      case *cancelCtx:
         return c, true
      case *timerCtx:
         return &c.cancelCtx, true
      case *valueCtx:
         parent = c.Context
      default:
         return nil, false
      }
   }
}

   它还返回一个函数指针,这个函数指针实际上就是执行cancelCtx中的cancel函数--c.cancel(true, Canceled),该操作会解除和parent ctx的依赖。

   总的来说创建一个新的context就是在parant context中挂载一个 children context,也许传入的parent与新生成的ctx会挂载到同一个ctx下,也许会加入到parent contxt的children 队列中。我们要与上面的四种类型的context比较,empty context和 value context是不具备挂载children的能力的,而cancel context 和timer context 两种类型具备挂载chidren 的能力(实际上timerCtx的挂载能力是继承于canelCtx)。

  但问题来了,在创建cancel context时候需要传入一个parent 参数,那么这个parent从哪里来?这时候就需要 func Background() Context 这个函数,它返回一个作为起始点的context对象,而这个BackgroundCtx是一个empty context,这就是empty context的作用。在回想一下上面的介绍是不是很合理的构造?

WithDeadline

  复制parentCtx,在创建的时候如果parentCtx已经触发Deadline,则直接返回一个cancelCtx和Cancel函数,否则会将返回一个timeCtx和Cancel函数。在发生以下情况的时候Done信号会被触发:

  1)Cancel函数被调用

  2)当前时间超过设置的Deadline

  3)parentCtx触发了Done信号

  

 

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(true, 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) }
}

 

WithValue

  复制parentCtx,保存传入的key-value键值对,没有Cancel函数被返回,但不代表该context没有done信号,只有在parentCtx的Done信号被触发的时候,才会发送。

func WithValue(parent Context, key, val interface{}) Context {
	if key == nil {
		panic("nil key")
	}
	if !reflect.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

  

 

 

 3.2 、Context的应用示例

 

参考网址   

[1] https://godoc.org/golang.org/x/net/context

  

posted @ 2018-05-31 19:26  王的博客  阅读(1620)  评论(0编辑  收藏  举报