glang context end - source code analysis

summary

The context package is golang1 6 start to provide the context package, golang1 7 move into the standard library. Context is only used in grpc, but it is not really used in its own program, and it does not understand its purpose. You must end with the context.

Main role

It mainly solves the problem that multiple goroutines and multiple links are nested and cannot be terminated (goroutine leakage problem) and the problem of context data sharing. In fact, it mainly solves the problem of goroutine termination, which is less used for general context data sharing.

realization

The implementation method is to close the transmission blocking of chancel through timeout, deadline, manual and other methods, complete the semaphore transmission, and realize goroutine termination. The source code implementation is that the context sends signals by manually or automatically calling the cancel method, and then the sub goroutine calls the Done method to receive signals. For the specific implementation, please refer to the context source code analysis

Context package source code analysis

common method

There are four main methods used

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
  • If WithCancel sends the termination information manually, the CancelFunc method needs to be called manually
  • WithDeadline sends in a precise deadline, such as (2021-8-2). When it reaches this time, the termination signal will be sent automatically
  • WithTimeout passes in a countdown time, such as 100s. After 100s, the termination signal will be sent automatically
  • WithValue passes in key and value values to facilitate context data sharing

Context interface

All the above four methods must be passed to context, which is a set of interfaces. Context defines a series of interfaces:

//Multiple goroutine s can call methods of the context at the same time.
type Context interface {
  Deadline() (deadline time.Time, ok bool)
  Done() <-chan struct{}
  Err() error
  Value(key interface{}) interface{}
}

  • Deadline returns an end time point. ok indicates whether there is an end time point

  • Done Context terminates the signal, and its signal transmission method is implemented by chancel

  • Err returns the reason for the end of the Context. It will only be returned when Done returns

- when the manual termination is cancelled, the cancelled error is returned

- DeadlineExceeded error is returned when timeout termination is cancelled

  • Value is only used when using the WithValue method. Generally, the superior passes in KV through the WithValue method, and the value is obtained through K.

Root Context

How to use the Context interface method? You need to initialize a root Context With, implement the Context interface internally through the four common With methods, and then pass the Context.

There are two implementations of root Context:

func Background() Context
func TODO() Context
  • Background returns an empty Context, which is usually used for initialization in the main function and is generally used as a top-level Context. It has no expiration time, cannot be cancelled, and has no value

  • TODO also returns an empty Context, which is used when it is unclear which Context to use

The source code is:

type emptyCtx int
....
var (
  background = new(emptyCtx)
  todo       = new(emptyCtx)
)
func Background() Context {
  return background
}

func TODO() Context {
  return todo
}

Both return an empty emptyCtx Context.

Four context s

There are four context s for different uses:

They are emptyCtx, cancelCtx, timerCtx and valueCtx

All four contexts implement Context interface, which is convenient for calling

emptyCtx

emptyCtx is an empty Context, and the return of the function is empty. It is mainly used on the Background

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
}

cancelCtx

CancelCtx is used in the WithCancel method. In the WithCancel method, a cancelCtx Context is created through newCancelCtx(Context), the main goroutine is then invoked by cancel() method of cancelCtx to send the stop signal, and the sub goroutine terminates the signal by means of the cancel() method of the cancelCtx.

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) Value(key interface{}) interface{} {
  if key == &cancelCtxKey {
    return c
  }
  return c.Context.Value(key)
}

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()
  err := c.err
  c.mu.Unlock()
  return err
}
// 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)
  }
}

timerCtx

timerCtx is used on the WithDeadline and WithTimeout methods. The usage of these two methods is almost the same. They both send the termination signal automatically through the arrival of time. timerCtx Context is through timer time Afterfunc, you can view the WithDeadline source code

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
  if parent == nil {
    panic("cannot create context from nil parent")
  }
  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) }
}
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 contextName(c.cancelCtx.Context) + ".WithDeadline(" +
    c.deadline.String() + " [" +
    time.Until(c.deadline).String() + "])"
}

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

valueCtx

valueCtx is mainly used in the WithValue method, which is used for context data sharing.

The same Context is used for data sharing. In the case of the same key, the child Context will overwrite the value of the parent Context

type valueCtx struct {
  Context
  key, val interface{}
}

func (c *valueCtx) String() string {
  return contextName(c.Context) + ".WithValue(type " +
    reflectlite.TypeOf(c.key).String() +
    ", val " + stringify(c.val) + ")"
}

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

Briefly summarize the four context s:

  1. They all implement the Context interface

  2. emptyCtx is an empty Context used to initialize the root Context

  3. cancelCtx internally implements the Context of sending termination signals, mainly in the cancel method

  4. timerCtx is the Context for sending due time and timeout, and inherits cancelCtx, which really calls the cancel method of cancelCtx

  5. valueCtx is the Context used for Context data sharing

Sending and receiving of semaphores

Take WithCancel for example. The return of WithCancel is a Context interface and cancelfunction. Cancelfunction is a function type. Once the cancelfunction is called, it will send a termination signal.

The method corresponding to the cancelfunction is cancel()

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

It mainly depends on these two interfaces. cancelCtx and timerCtx both implement these two methods

type canceler interface {
  cancel(removeFromParent bool, err error) //Send signal
  Done() <-chan struct{} //Receive signal read chan
}

Let's mainly look at the implementation of the cancel method of cancelCtx:

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  //Send an empty struct to chan: var closedchan = make(chan struct {})
  } else {
    close(c.done) 
  }
  for child := range c.children {
    // NOTE: acquiring the child's lock while holding parent's lock.
    child.cancel(false, err) //Close sub Context
  }
  c.children = nil
  c.mu.Unlock()

  if removeFromParent {
    removeChild(c.Context, c)
  }
}

Keywords: Go Context

Added by clairence on Fri, 17 Dec 2021 06:22:54 +0200