Illustrate the context s of the Go Language to understand the source code for the core implementation of the programming language

Foundation Building

Some designs in thread-based programming languages

ThreadGroup

ThreadGroup is a common concept in programming languages that are based on thread concurrency. When a thread spawns a child thread, it usually joins the parent thread's thread group (without specifying a thread group). ThreadGroup can ultimately control the exit of a group of threads, etc. Then goroutine does not have a clear parent/child relationship in the go language, if you want toContext is required to exit all goroutines on the current call chain

ThreadLocal

In thread-based programming languages, ThreadLocal is often used to store threads locally. Essentially, a Map is used to store key/value. There is no ThreadLocal design in go. When key/value is passed, context information can be passed as well as parameters.

context Typical Scenarios

scene Realization principle
Context Information Transfer WithValue Keyvalue pairs are saved by an internal key/value attribute, which cannot be modified but can only be replaced by an override
Exit Notification WithCancel Common exit notification by listening for notification channel s

Recursive acquisition of context data

Since map is not used for data saving in go's context, when actually getting it, it starts from the current level and recursively goes up one by one until a matching key is found

In fact, we are analogous to ThreadGroup, because goroutine itself does not have a subordinate or subordinate concept, but we can actually use context to achieve the parent-child relationship of data transfer. We can set context data in a goroutine and pass it to the derived goroutine.

Cancellation notice

Now that the parent/child parent-child relationship is built through contexts, contexts register themselves with parents during implementation. When we cancel a parent's goroutine, we actually recursively cancel the done chan of its child context s layer by layer, causing all cancel-listening goroutines in the entire call chain to exit

What if the done chan of a child context is initialized?So how do I tell you to close? Just give you a closedchan that's already closed. Is that OK?

With timeout context

To achieve a timeout control, through the parent/child mechanism of the context above, we only need to start a timer, and then when the timeout occurs, we can cancel the current context directly, so that we can exit the goroutine listening for the current and lower context.Done()

Background and TODO

Backgroud is easy to understand literally. In fact, it constructs a context object as the root object, which is essentially a shared global variable. Usually in some system processes, we can use this object as the root object and construct a new context to transfer context data and unify exit control.

What about TODO?Usually we set up a lot of todo lists for ourselves. Actually, the same thing happens here. Although we build a lot of todo lists, most people don't do anything. They pass them through many function calls, but they usually don't use them. For example, you won't listen for exit or get data from them. TODO, like Background, returns one behind itglobal variable

immutability

Usually we use contexts as a context for data transfer, such as processing an http request request, but if this request is processed, its context is meaningless and should not be reused in the future. If it has timed out or been canceled before, its state will not change

Source implementation

context interface

type Context interface {
    // Deadline returns an expired timer timer and whether and when it expires
    Deadline() (deadline time.Time, ok bool)

    // Done returns a closed channel when the current context is complete, indicating that the current context should be cancelled for goroutine to clean up
    // WithCancel: Responsible for closing Done when cancel is called
    // WithDeadline: Responsible for closing Done when the final deadline expires
    // WithTimeout: Responsible for closing done after timeout
    Done() <-chan struct{}

    // Return to nil if Done channel is not closed
    // Otherwise, a specific error will be returned
    // Canceled canceled
    // DeadlineExceeded expires
    Err() error
	// Returns the value of the corresponding key
    Value(key interface{}) interface{}
}

emptyCtx

emptyCtx is a context implementation that will not be cancelled, has no expiration time, has no value, and will not return an error. It mainly returns this root context as context.Background() and context.TODO() or does nothing.

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

Compare the String method of emptyCtx when implementing interesting implementations, which returns the specific type of the current context, such as Background or TODO, because background and todo are two global variables whose addresses are used to determine the corresponding type

cancelCtx

structural morphology

The cancelCtx structure contains a Context object, its parent context, which also saves all contexts that can be cancelled internally through children. When the current context is canceled, only the context of all canceler interfaces needs to be called to cancel the current call chain.

type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields protection properties
    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
}

Done

The Done operation returns a current chan to notify goroutine to exit


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
}

cancel

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    // Once a context is cancelled by an action, no state modifications are made
    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 current chan
        close(c.done)
    }
    // Call all children s to cancel
    for child := range c.children {
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    // Whether it is necessary to remove from the parent context, or if it is the cancel action of the current context, it is required
    // Otherwise, the parent context will actively remove the child
    if removeFromParent {
        removeChild(c.Context, c)
    }
}

timerCtx

timerCtx is primarily used to implement WithDeadline and WithTimer context ual implementations, which inherit the cancelCtx interface and include a timer.Timer timer and a deadline termination implementation

2.4.1 Structures

timerCtx

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Timer timer

    deadline time.Time //Termination Time
}

Cancel Method

Cancellation is a simple process of cancelCtx cancellation followed by a Stop operation on its own timer so that cancellation can be achieved

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() // Stop Timer
        c.timer = nil
    }
    c.mu.Unlock()
}

valueCtx

It is saved internally through a key/value, and if the current context does not contain a value it will recurse up one level at a time

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

propagateCancel

design goal

The main design goal of propagateCancel is to cancel the child context when the parent context cancels, which has two modes: 1. Notify child to cancel when parent cancels 2. Call child's hierarchical recursive cancellation when parent cancels

parentCancelCtx

context can be arbitrarily nested into a N-tier tree structure. Combining the above two modes, the second mode is used when parent can be found to be either cancelCtx or timerCtx. Parent calls cancel of child to complete the exit of the whole call chain, and the other mode listens to Done.

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	for {
		switch c := parent.(type) {
		case *cancelCtx:
			return c, true	// Find the parent that recently supported cancel, called by the parent to cancel
		case *timerCtx:
			return &c.cancelCtx, true // Find the parent that recently supported cancel, called by the parent to cancel
		case *valueCtx:
			parent = c.Context // recursion
		default:
			return nil, false
		}
	}
}

Core implementation

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
            // Cancel directly if parent is found to have been cancelled
			child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
            // Otherwise add to parent's children map
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		go func() {
			select {
			case <-parent.Done():
                // Listening for parent DOne completion, parent will not be registered here
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

WithDeadline

With the above basic learning WithDeadline, it is much simpler. WithDeadline will set a deadline for calculating how long to wait for cancellation from the current 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,
	}
    // Listen for parent cancellations or register yourself with parent
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
        // Has expired
		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() {
            // Build a timer timer that automatically calls cancel to cancel when it expires
			c.cancel(true, DeadlineExceeded)
		})
	}
    // Return Cancel Function
	return c, func() { c.cancel(true, Canceled) }
}

Backgroup and TODO

Backgrouping and Todo are the most common forms of information passed through contexts in many low-level middleware calls. Although they are based on emptyCtx implementation, Backgrouping tends to use root as a parent context for subsequent entire call chain contexts, whereas TODO typically indicates no subsequent action, simply because parameters need to be passedDelivery >Links to the original text http://www.sreguide.com/go/context.html >Microsignal: baxiaoshi2020 >Focus on bulletin numbers to read more source analysis articles >More articles www.sreguide.com >This article is a multi-article blog platform OpenWrite Release

Tags: Programming Attribute Go

Posted on Mon, 06 Jan 2020 00:21:21 -0800 by vincente