Make writing a habit together! This is the second day of my participation in the “Gold Digging Day New Plan · April More text challenge”. Click here for more details. The context epsilon (┬ ┬ man ┬ ┬) 3

Context

In the Go service, a separate Goroutine is usually used to handle a request, but in this goroutine, other Goroutines may be opened to perform specific transactions, such as database, RPC, etc. At the same time, This group of Goroutines may also need to share access to special values, such as user tokens, request expiration time, etc. When a request times out, we want all goroutines associated with the request to exit quickly to reclaim system resources.

The Context package, which is open sourced by Google and added to the standard library in Go 1.7, makes it easy to pass specific values, cancel signals, and expiration dates to all goroutines involved in the request.

The core of the context package is the Context interface, which is structured as follows:

type Context interface {
    Done() <-chan struct{}
    Err() error
    Deadline() (deadline time.Time, ok bool)
    Value(key interface{}) interface{}}Copy the code
  1. DoneReturns achanRepresents a cancel signal. When the channel is closed, the function should immediately finish and return.
  2. Err()Returns aerror, indicating the reason for removing the context
  3. DeadlineReturns the time when the context was cancelled
  4. ValueUsed to get from the contextkeyThe value of the corresponding

use

Cancelation signals

As with chan to control concurrency, we want to pass a signal to the Goroutine that, once received, will immediately stop working and return. The context package provides a WithCancel(), which makes it easy to pass the cancel signal.

func useContext(ctx context.Context, id int) {
    for {
        select {
        case <- ctx.Done():
            fmt.Println("stop", id)
            return
        default:
            run(id)
        }
    }
}

func G2(ctx context.Context) {
    nCtx, nStop := context.WithCancel(ctx)
    go G4(nCtx)
    for {
        select {
        case <- ctx.Done():
            fmt.Println("stop 2")
            nStop()
            return
        default:
            run(2)}}}func G3(ctx context.Context) {
   useContext(ctx, 3)}func G4(ctx context.Context) {
    useContext(ctx, 4)}func main(a) {
    ctx, done := context.WithCancel(context.Background())
    go G2(ctx)
    go G3(ctx)
    time.Sleep(5*time.Second)
    done()
    time.Sleep(5*time.Second)
}

Copy the code

Set a deadline

func G6(ctx context.Context) {
    for  {
        select {
        case <- ctx.Done():
            t, _ := ctx.Deadline()
            fmt.Printf("[*] %v done: %v\n", t, ctx.Err())
            return
        default:
            fmt.Println("[#] run ...")}}}func main(a) {
    // ctx, done := context.WithTimeout(context.Background(), time.Second * 2)
    ctx, _ := context.WithTimeout(context.Background(), time.Second * 2)
    go G6(ctx)
    //done()
    time.Sleep(10*time.Second) } [#] run ... . [*]2020- 10- 31 20:24:42.0581352 +0800 CST m=+2.008975001 done: context deadline exceeded
Copy the code

The value of

func G7(ctx context.Context) {
    for  {
        select {
        case <- ctx.Done():
            fmt.Println("cancel", ctx.Value("key"))
            return
        default:
            fmt.Println("running ", ctx.Value("key"))
            time.Sleep(time.Second)
       }
    }
}

func main(a) {
    ctx, _ := context.WithTimeout(context.Background(), time.Second * 2)
    ctx =  context2.WithValue(ctx, "key"."value")
    go G7(ctx)
    time.Sleep(10*time.Second)
}
Copy the code

The context package overview

The core of the context package is the context. context interface, and there are four structs that implement the context interface. EmptyCtx cancelCtx timerCtx valueCtx emptyCtx cancelCtx timerCtx valueCtx emptyCtx cancelCtx timerCtx valueCtx The context package exposes two methods, Background() and TODO(), to create an empty struct, and a total of four methods to generate an emptyCtx for each of the following structs: WithCancel(), WithDeadLine(), WithTimeout(), WithValue(), the corresponding relationship is as follows:

TODO and Background

The TODO and Background methods are used to return an emptyCtx type. They are implementatively the same:

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

func Background(a) Context {
	return background
}

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

Both methods return a non-empty context emptyCtx, which is never cancelled and is passed to other methods to build more complex context objects. Background() is generally used by default, and TODO() is used only when in doubt, but in reality they are just different names.

Here is an implementation of emptyCtx, which does nothing.

type emptyCtx int

func (*emptyCtx) Deadline(a) (deadline time.Time, ok bool) {
   return
}

func (*emptyCtx) Done(a) <-chan struct{} {
   return nil
}

func (*emptyCtx) Err(a) error {
   return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
   return nil
}
Copy the code

WithCancel

type cancelCtx struct {
	Context

	mu       sync.Mutex            // For synchronization
	done     chan struct{}         // Will be returned in Done
	children map[canceler]struct{} // List of subcontexts. When done is closed, the map is traversed and all subcontexts are closed
	err      error                 // Turn off the exception generated by chan, which is not null when initialized
}

func (c *cancelCtx) Done(a) <-chan struct{} {
	c.mu.Lock()
	if c.done == nil {
		c.done = make(chan struct{})
	}
	d := c.done
	c.mu.Unlock()
	return d
}
Copy the code
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func(a) { c.cancel(true, Canceled) }
}
Copy the code

When WithCancel is called, a new cancelCtx is first copied from parent:

func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}
Copy the code

We then call propagateCancel to terminate the child context when the parent context ends, and return func in addition to the cancelCtx reference. C.ancel () is called, where done() is called, C. canel ()

cancel

Cancel closes the cancelctx. done pipeline for the current and subcontexts.

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    // There must be a reason for closing
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	ifc.err ! =nil {
		c.mu.Unlock()
		return     // Closed, return
	}
	c.err = err    // Indicates that err is off
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done)   // Close the current done
	}
    // Since it is a map, the closing order is random
	for child := range c.children {
		child.cancel(false, err)   // Iterate to cancel all subcontexts
	}
	c.children = nil    // Remove the subcontext
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)   // Delete yourself from the parent context}}Copy the code

propagateCancel

This function ensures that the child context ends when the parent context ends. On the one hand, the child is closed if the parent has been cancelled during the generation of the child context. On the other hand, if the parent context is kept open during execution, the child is also closed. Add the child to the children list of the parent, perform cancel, and then close.

func propagateCancel(parent Context, child canceler) {
    done := parent.Done()
    // If the parent's Done method returns null, the parent context is never cancelled
    CTX, done := context.withcancel (context.background ())
    if done == nil {
        return 
    }
    
    // If the parent context has been cancelled, close the current context
    select {
    case <-done:
        child.cancel(false, parent.Err())
        return
    default:}// Father is not cancelled
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        // The father has cancelled and closed himself
        ifp.err ! =nil {
            child.cancel(false, p.err)
        } else {
            // Add child to parent's children
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        // Parent context is a developer-defined type that enables a Goroutine to listen for parent and child contexts until one of them is closed
        atomic.AddInt32(&goroutines, +1)
        go func(a) {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}
Copy the code

WithTimeout and WithDeadline

type timerCtx struct {
    cancelCtx
    timer *time.Timer
    deadline time.Time
}

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

TimerCtx is implemented by adding a timer and expiration time to cancelCtx.

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    // If the deadline passed in is later than the parent context's deadline, that means the parent context must end before the child
    // It makes no sense to set a cut-off time for the subcontext in this case, so a cancelCtx is created directly
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		return WithCancel(parent)
	}
    // Build a new timerCtx
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
    // Ensure that the child is closed when the parent is closed
	propagateCancel(parent, c)
    // Calculate how long it is until d
	dur := time.Until(d)
    // Close the subcontext if the cutoff time has passed
	if dur <= 0 {
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func(a) { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
    // c.err == nil indicates that the current context is not closed
	if c.err == nil {
        // AfterFunc waits for the duR to open a goroutine that executes the passed method, c. canel
        // A timer is returned, which can be stopped and cancelled by calling the timer Stop method.
		c.timer = time.AfterFunc(dur, func(a) {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func(a) { c.cancel(true, Canceled) }
}
Copy the code

The cancel method of timerCtx mainly calls cancelctx.cancel

func (c *timerCtx) cancel(removeFromParent bool, err error) {
    Cancelctx. cancel to close the subcontext
    c.cancelCtx.cancel(false, err)
    // Remove the current context from the parent context
    if removeFromParent {
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    ifc.timer ! =nil {
        // Stop the timer and cancel the call
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}
Copy the code

WithTimeout calls WithDeadline directly

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}
Copy the code

WithValue

func WithValue(parent Context, key, val interface{}) Context {
    // Key cannot be nil
	if key == nil {
		panic("nil key")}// Key must be comparable
	if! reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}

type valueCtx struct {
	Context
	key, val interface{}}Copy the code

The provided key must be comparable and should not be of type string or any other built-in type to avoid collisions between packages using context. Users of WithValue should define their own types for keys.

Use custom struct{} as much as possible for key. Avoid using built-in data types to avoid conflicts when using context packages

conclusion

Context package is a model for concurrency control in complex scenarios added after Go 1.7. The core interface is context. context. This structure defines five methods to be implemented, which are used to send shutdown signals, set dateline, transfer values and other functions.

The core idea of the context package is to organize goroutine in a tree. When creating a new context, you need to specify a parent context. Therefore, the root context corresponds to the root Goroutine, and the sub-context corresponds to the sub-Goroutine, achieving flexible concurrency control.

RootContext is typically created from Background() or TODO(), which creates an empty emptyCtx, and then WithCancel() if you want to use the specific functionality of the context package, WithDateline() or WithValue() wraps the parent context into a concrete context object (cancelCtx, timerCtx, valueCtx), The first two methods return two values (CTX Context, done func()). Calling done sends a close signal to the Goroutine, which is obtained by monitoring ctx.done () in the Goroutine.

CancelCtx and timerCtx will remain the same children (timerCtx is actually a descendant of cancelCtx). This is a map. The key is canceler and the Value is struct{}. CancelCtx or timerCtx creates cancelCtx or timerCtx, which adds the current context to its parent’s children. When the parent closes, it traverses the children to close all the children and removes this context from its parent’s children. Due to the unordered nature of the map traversal, the order in which subcontexts are closed is also random.

The implementation of WithValue() and valueCtx is slightly different from the first two. On the one hand, valueCtx does not implement Done(), Deadline() and other methods by itself, so its function is limited to value transfer. There is no call to propagateCancel() in WithValue(), so valueCtx is not put in the children of the parent context, and valueCtx has no children of its own, so using valueCtx as the parent context makes no sense.

If not necessary, the WithValue() function is not usually used to pass the value, which is usually used to pass the authentication token of the user corresponding to the request or the request ID used for distributed tracing.