Go Context parsing A Brief Inquiry Into Go Context

What is context

Package context defines the Context type, which carries deadlines,

cancellation signals, and other request-scoped values across API boundaries

and between processes.

By introducing context in the context package, you can easily see the three main functions of context

  • Carry deadline
  • Carry cancel signal
  • Carry the value related to the request

The scope is between api boundaries and processes

Why do I need context

It can be seen from the introduction of context that context is mainly used for process cancellation or concurrency control, and value transfer is an additional function.

Go is synchronously similar to C, but with memory safety, garbage collection, structural typing, and CSP style concurrency. The last item deals with concurrency, which is a major feature of go language, which can understand why go needs context as an important part of concurrency control.

As we all know, Go language has four tools for concurrency control

  • global variable
  • channel
  • waitgroup
  • context

To understand why context is also an indispensable part, you might as well ask a question: what would happen if there was no context?

// Without cancel
func withoutCancel() {
	go func() {
		go func() {
			defer fmt.Println("child dead")
			for {
				fmt.Println("child running...")
				time.Sleep(1 * time.Second)
			}
		}()
		for i := 0; i < 2; i++ {
			fmt.Println("father running...")
			time.Sleep(1 * time.Second)
		}
		defer fmt.Println("father dead")
	}()
	time.Sleep(5 * time.Second)
}
output:
father running...
child running...
child running...
father running...
father dead
child running...
child running...
child running...
child running...

Obviously, after the parent collaboration is over, the child collaboration is still running, and there is no normal exit in the end. Is there any substitute to cancel the collaboration?

Global variables? Obviously, if the control of each service request is controlled by the same global variable, it is easy to block.

waitgroup is designed to control multi process synchronization, and it seems not suitable to control a single process.

Then you can only use channel to control a single process. Try it

// withChannel cancels the co process using the channel
func withChannel() {
	go func() {
		c := make(chan bool)
		defer fmt.Println("father dead")
		go func() {
			defer fmt.Println("child dead")
			for {
				select {
				case <-c:
					return
				default:
					fmt.Println("child running...")
					time.Sleep(1 * time.Second)
				}
			}
		}()
		for i := 0; i < 2; i++ {
			time.Sleep(1 * time.Second)
			fmt.Println("father running...")
		}
		c <- true
	}()
	time.Sleep(5 * time.Second)
}
output:
child running...
father running...
child running...
child running...
father running...
child dead
father dead

The effect here looks good. The father process notifies the child of the end of the process by putting a value into the channel.

What if there are multiple processes? Obviously, it is impossible to insert multiple values into one channel. Multiple consumption associations will compete and cannot be managed effectively. If a process corresponds to a channel, the resource consumption of creating multiple channels will not be mentioned. The management and use of multiple channels will become extremely chaotic and complex when the number of processes increases. context can solve this problem simply and gracefully.

context source code and design

Interface

First look at the context interface

type Context interface {
    Deadline() (deadline time.Time, ok bool) // Returns the time when the context was cancelled
    Done() <-chan struct{} // Returns a channel that will be closed after the current work is completed or the context is cancelled
    Err() error // Return the reason why the context ended
    Value(key interface{}) interface{}
}

In the context package, four important classes implement this interface respectively

  • emptyCtx
  • valueCtx
  • cancelCtx
  • timerCtx

These four classes realize the powerful function of context respectively

emptyCtx

type emptyCtx int

emptyCtx is an empty context. It cannot be cancelled. There is no deadline and no value.

context.Background() and context.TODO() return an emptyCtx

valueCtx

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

valueCtx is a context used to carry key value. You can see that only one key and one value are stored in a valueCtx. How does context store multiple kv? You can see how func WithValue is implemented

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

It can be seen that there is also a parentCtx in each valueCtx. When multiple values need to be saved in the context, it is actually a linked list of valueCtx. context.Value() actually searches kv by traversing the linked list. It can be easily verified by looking at the implementation of context.Value().

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

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
}

cancelCtx is mainly used to cancel the context. Like valueCtx, a cancelCtx contains a parentCtx, which indicates that cancelCtx also has an inheritance relationship. In addition, it also contains a children map, which looks like a tree structure. You can observe how to use children later. And a channel to implement the channel returned in context.Done(), a mutex to ensure concurrency security, and an err to record the reason for cancellation.

With cancel() function can generate a cancelCtx. Let's see how it is implemented

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) }
}
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

You can see that, like valueCtx, save parentCtx to cancelCtx first, and then perform an action of propagateCancel propagation cancellation. Let's take a look at how the propagateCancel is implemented

func propagateCancel(parent Context, child canceler) {
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err())
		return
	default:
	}

	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 {
		atomic.AddInt32(&goroutines, +1)
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

The first few steps are very simple. They are some checks on whether the parent has ended.

p. OK: = parent cancelCtx (parent) is always looking up from the parent to see if the parent also inherits from another cancelCtx

If the parent does inherit from a cancelCtx, mount the new child under the children map of this cancelCtx.

If not, create a new collaboration to monitor whether the parent is cancelled. If the parent is cancelled, the child will also be cancelled

It can be seen that cancelCtx is actually a tree structure. When parentCtx is cancelled, the child ctx in the children map will also be cancelled.

timerCtx

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

	deadline time.Time
}

After learning about cancelCtx, timerCtx is very simple. A timerCtx contains a cancelCtx to perform cancellation, a timer and a time to record the end time

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

It is also easy to create a timerCtx. It can be seen that most of them are almost the same as creating a cancelCtx. Just create a timer timer to cancel.

Think: is context really a good design?

So far, I believe you should know how context is implemented.

Context has always been regarded as a small and beautiful design, and the context package does realize these functions of context in a clever way. However, there are still some points worth considering. The following views are not all from individuals and are for discussion only

Context everywhere! Context spreads like a virus in go code. Even unnecessary code needs to pass context.

I believe you have asked or been asked a question. What does context pass in this function? Answer: just pass context.TODO(). For no reason, just pass it.

It is precisely because context spreads everywhere in the go code that context.TODO() is such an incredible thing

Is valueCtx necessary? Why use a linked list to implement a map?

At the beginning of the context package, there is an introduction to use context values only for request scoped data, that transits processes and APIs, not for passing optional parameters to functions

I believe you have seen the behavior of stuffing the whole structure into the context for transmission in the readability test, but how to define which data can be put into the context and which can not? Our current log plug-ins, error plug-ins, have also stored these information in the context. These are not the so-called request scoped data, which is also a point worthy of discussion. Last but not least

Does ctx context.Context conform to readability?

Reference

Tags: Go

Posted on Fri, 26 Nov 2021 12:19:35 -0500 by bailo81