introduce

In the Go server, each incoming request is processed in its own Goroutine. Request handlers typically start other Goroutines to access the back end, such as databases and RPC services. The Goroutine collection that processes the request typically requires access to request-specific values, such as the identity of the end user, authorization token, and expiration date of the request. When a request is cancelled or times out, all goroutines handling the request should exit quickly so that the system can reclaim any resources they are using.

Context is designed to simplify operations related to data sharing, cancellation of signals, and timeout handling for a single request between multiple Goroutines. Go Concurrency Patterns: Context.

features

  • Context is Gorountine concurrency safe.
  • Supports a tree-like superior to control one or more subordinates, but does not support reverse control and level control.
  • Gorountine delivers the cancel signal, ending the child Gorountine’s life.
  • Gorountine Initializes the initiator Gorountine service, passing in a cutoff time or timeout period to control the child Gorountine.
  • The Gorountine ends, and all of the corresponding child Gorountine lifecycles end.

Usage scenarios

  • In the case of concurrent multi-service invocation, for example, a request comes in and three Goroutines are started to invoke RpcA, RpcB, and RpcC services. As long as one of the services fails, an error is returned and the other two Rpc services are cancelled. This can be done with the WithCancel method.
  • Timeout requests, such as Http and Rpc, can be implemented WithDeadline and WithTimeout.
  • Carrying data, such as a requested user information, for general business scenarios, we will have a special middleware to validate the user information, and then inject the user information into the context, or share it with multiple derived Goroutines, which can be implemented using the WithValue method.

The official sample

package main
import (
	"context"
	"fmt"
)

func main() {
	gen := func(ctx context.Context) <-chan int {
		dst := make(chan int)
		n := 1
		go func() {
			for {
				select {
				case <-ctx.Done():
					return // returning not to leak the goroutine
				case dst <- n:
					n++
				}
			}
		}()
		return dst
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // cancel when we are finished consuming integers

	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
			break}}}Copy the code

Context

Context is an interface defined as follows: source code

type Context interface {
	Deadline() (deadline time.Time, ok bool)

	Done() <-chan struct{}

	Err() error

	Value(key interface{}) interface{}
}
Copy the code

Contains the following three functions:

  • Time to live
  • Cancel the signal
  • Value is shared between gorouting derived from Request

EmptyCtx is a Context implementation that implements Deadline, Done, Err, Value, and String methods, respectively.

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"
}
Copy the code

cancelCtx

From the example, we see the first initialization that needs to be done using the context

ctx, cancel := context.WithCancel(context.Background())
Copy the code

In this case, context.background () returns a pointer to emptyCtx.

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

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}
Copy the code

Again, the WithCancel function takes background as an argument and creates an instance of cancelCtx. And it has Context as one of its anonymous fields, so it can be treated as a Context.

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

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
}
Copy the code

As you can see from the figure below, the primary responsibility of WithCancel is to create a cancelCtx, mount itself to the parent node, and then return the cancelCtx and cancel() function that triggers the cancellation event.

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
}

func (c *cancelCtx) String() string {
	return fmt.Sprintf("%v.WithCancel", c.Context)
}
Copy the code

More importantly, cancelCtx implements the cancel() method. The main working process is as follows:

  1. Set the cancellation error message
  2. Close the channel: c.d one
  3. Recursively close all child nodes
  4. Deletes itself from the parent node
  5. Finally, the cancellation signal is passed to all child nodes by closing a channel
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error"} c.mlock () // Has been cancelledifc.err ! = nil { c.mu.Unlock()return} // Set cancelCtx error message c. errr = errif c.done == nil {
		c.done = closedchan
	} else{close(c.one)} // Recursively cancel all child nodesfor child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)} // Clear child nodes c.dren = nil c.mu.nlock ()ifRemoveFromParent {// Remove self from parent node removeChild(c.text, c)}}Copy the code

There is also an important function to focus on, propagateCancel

Func propagateCancel(Parent Context, Child canceler) {// The parent node is an empty node, propagateCancel(Parent Context, Child canceler) {// The parent node is the root node and does not need to be mountedif parent.Done() == nil {
		return// Parent is never canceled} // The parent node is cancelableif p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		ifp.err ! Canceled = nil {// Parent has already been canceled // The parent node is canceled, and the parent node also needs to cancel child.cancel(false, p.err)
		} else {
			ifP.cldren == nil {p.cldren = make(map[canceler]struct{})} p.cldren [child] = struct{}{}} p.cldren () }else{// For compatibility, go occurs when Context is embedded within a typefunc() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}
Copy the code

Here is an example of the else case in the above code. Why do we need to open a Goroutine to monitor the cancellation signal?

case <-parent.Done():
Copy the code

CancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx cancelCtx

type CancelContext struct {
    Context
}
Copy the code

ParentCancelCtx cannot recognize CancelContext as cancelable CTX. Let’s look at the second case:

case <-child.Done():
Copy the code

The main function is to enable the SELECT statement to exit normally when the child node is cancelled, avoiding goroutine leakage.

ParentCancelCtx: parentCancelCtx: parentCancelCtx: parentCancelCtx: parentCancelCtx: parentCancelCtx

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}}}Copy the code

timerCtx

From the definition, we can see that timerCtx is implemented based on cancelCtx, with additional two fields: Timer and Deadline. And timerCtx implements its own Deadline method.

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
}
Copy the code

So let’s go straight to the core function WithDeadline

Func WithDeadline(parent Context, d time.time) (Context, CancelFunc)returnCancalCtx cancalCtx cancalCtx cancalCtx cancalCtx cancalCtx cancalCtx cancalCtx cancalCtx cancalCtx cancalCtx cancalCtxif cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		returnWithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: D,} propagateCancel(parent, c) dur := time. (dif dur <= 0 {
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(falseCanceled)}} c.u.lock () defer c.u.lock () // The core code is here and cancels automatically if the current node is not Canceled by calling the cancel function through time.afterfunc after dur time.if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(trueCanceled)}} // Cancel WithTimeout(parent Context, timeout time.duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout))
}
Copy the code

valueCtx

The source code is relatively simple, use Context as an anonymous field to implement the interface of the type list, pass the layer by layer, get the Value mainly look at the Value method, it will determine whether there is a Value layer by layer, until it finds the top-level Context. The key value of the child node overrides the value of the parent node, so be careful when naming the key.

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

// 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)
}
Copy the code

parsing

The Done method returns a <-chan struct{} used for message communication between goroutines.

The end of the

Welcome to Github.

reference

Go Concurrency Patterns: Context Go Context