Preface — Why do WE need Context

Golang Context is a concurrency control technique commonly used in Golang application development. The biggest difference between Golang Context and WaitGroup is that context has stronger control over the derivation of Goroutine, which can control multiple levels of Goroutine.

Context controls a tree of goroutines, each of which has the same context.

Typical usage scenarios are as follows:

When a request is cancelled or times out, all goroutines used to process the request should be quickly exited before the system can release the resources occupied by these Goroutines. In the figure above, since a goroutine descends from a child goroutine, which in turn descends from a new goroutine, using WaitGroup is not easy because the number of child Goroutines is not easy to determine. And that’s easy to do with context.

1. Exit using global variables

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup
var exit bool

// The problem with global variables:
// 1. Using global variables is not easy to unify when called across packages
// 2. If the worker starts goroutine again, it is not easy to control it.
func worker(a) {
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		if exit {
			break
		}
	}
	wg.Done()
}

func main(a) {
	wg.Add(1)
	go worker()
	time.Sleep(time.Second * 3) // sleep3 seconds to keep the program from exiting too quickly
	exit = true                 // Modify the global variable to exit the child goroutine
	wg.Wait()
	fmt.Println("over")}Copy the code

2. Exit by Channel

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

// There are problems with pipeline mode:
// 1. Using global variables is not easy to standardize and unify when calling across packages, and a common channel needs to be maintained
func worker(exitChan chan struct{}) {
LOOP:
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
		case <-exitChan: // Wait to receive the notification from the superior
			break LOOP
		default:
		}
	}
	wg.Done()
}

func main(a) {
	var exitChan = make(chan struct{})
	wg.Add(1)
	go worker(exitChan)
	time.Sleep(time.Second * 3) // sleep3 seconds to keep the program from exiting too quickly
	exitChan <- struct{} {}// Send exit signal to child goroutine
	close(exitChan)
	wg.Wait()
	fmt.Println("over")}Copy the code

3. Exit in Context mode

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
		case <-ctx.Done(): // Wait for notification from superior
			break LOOP
		default:
		}
	}
	wg.Done()
}

func main(a) {
	ctx, cancel := context.WithCancel(context.Background())
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 3)
	cancel() // Notify child goroutine end
	wg.Wait()
	fmt.Println("over")}Copy the code

And when a child goroutine opens another goroutine, just pass CTX:

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
	go worker2(ctx)
LOOP:
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
		case <-ctx.Done(): // Wait for notification from superior
			break LOOP
		default:
		}
	}
	wg.Done()
}

func worker2(ctx context.Context) {
LOOP:
	for {
		fmt.Println("worker2")
		time.Sleep(time.Second)
		select {
		case <-ctx.Done(): // Wait for notification from superior
			break LOOP
		default:}}}func main(a) {
	ctx, cancel := context.WithCancel(context.Background())
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 3)
	cancel() // Notify child goroutine end
	wg.Wait()
	fmt.Println("over")}Copy the code

The Context first

Go1.7 adds a new standard library context, which defines the Context type specifically designed to simplify operations related to data, cancellation signals, deadlines, and other aspects of the request domain between multiple Goroutines handling a single request, which may involve multiple API calls.

Incoming requests to the server should create context, and outgoing calls to the server should accept context. The chain of function calls between them must pass context, or a derived context that can be created with WithCancel, WithDeadline, WithTimeout, or WithValue. When a context is cancelled, all its derived contexts are also cancelled.

1. Context Implementation principle

The interface definition

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}}Copy the code

Among them:

  • The Deadline method needs to return the time when the current Context was cancelled, i.e. the Deadline for completion of the work.
  • The Done method returns a Channel, which is closed when the current work is Done or the context is cancelled. Multiple calls to the Done method return the same Channel.
  • The Err method returns the reason why the current Context ended. It will only return a non-empty value if the Channel returned by Done is closed.
    • A Canceled error is returned if the current Context is Canceled.
    • If the current Context times out, a DeadlineExceeded error is returned;
  • The Value method returns the Value of the Key from the Context. For the same Context, multiple calls to Value with the same Key return the same result. This method is only used to pass data across APIS and interprocess and request domains.

2. EmptyCtx – Background () and TODO ()

The context package defines an empty context, named emptyCtx, for the root node of the context. The empty context simply implements the context, contains no values of its own, and is used only by the parent node of another context.

The emptyCtx type is defined as follows:

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

The context package defines a common emptCtx global variable named background, which can be retrieved using context.background () as follows:

var background = new(emptyCtx)
func Background(a) Context {
    return background
}
Copy the code

Background() is used mainly in the main function, initialization, and test code as the top-level Context in the Context tree, which is the root Context.

TODO(), we can use this if we don’t know what Context to use.

Background and TODO are both essentially emptyCtx constructs, which are non-cancellable, have no expiration date, and carry no values.

CancelCtx cancelCtx timerCtx valueCtx struct (emptyCtx, timerCtx, valueCtx); If you don’t have a parent context for each of these methods, you pass in backgroud as its parent

  • WithCancel()
  • WithDeadline()
  • WithTimeout()
  • WithValue()

The relationship between context types in the context package is shown as follows:

3.cancelCtx–WithCancel()

Structure definition

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

Cancel () interface implementation

The cancel() inner method is the key to understanding cancelCtx. It closes itself and its descendants, which are stored in the cancelctx.children map. The key value is the descendant object of cancelctx.children.

The cancel method implementation code is as follows:

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	ifc.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)
	}
}
Copy the code

The WithCancel() method

The WithCancel() method does three things:

  • Initializes a cancelCtx instance
  • Add an instance of cancelCtx to the children of its parent (if the parent can also be cancelled)
  • Return the cancelCtx instance and cancel() method

The implementation source code is as follows:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)   // Add itself to the parent node
	return &c, func(a) { c.cancel(true, Canceled) }
}
Copy the code

The process of adding itself to the parent node needs to be explained briefly:

  1. If the parent node also supports cancel, which means that the parent node must have children members, then add the new context to children.
  2. If the parent does not support cancel, continue searching until you find a node that supports cancel and add the new context to children.
  3. If none of the parents support cancel, a coroutine is started to wait for the parent to terminate, and then terminate the current context.

Typical Application Cases

package main

import (
	"fmt"
	"time"
	"context"
)

func HandelRequest(ctx context.Context) {
	go WriteRedis(ctx)
	go WriteDatabase(ctx)
	for {
		select {
		case <-ctx.Done():
			fmt.Println("HandelRequest Done.")
			return
		default:
			fmt.Println("HandelRequest running")
			time.Sleep(2 * time.Second)
		}
	}
}

func WriteRedis(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("WriteRedis Done.")
			return
		default:
			fmt.Println("WriteRedis running")
			time.Sleep(2 * time.Second)
		}
	}
}

func WriteDatabase(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("WriteDatabase Done.")
			return
		default:
			fmt.Println("WriteDatabase running")
			time.Sleep(2 * time.Second)
		}
	}
}

func main(a) {
	ctx, cancel := context.WithCancel(context.Background())
	go HandelRequest(ctx)

	time.Sleep(5 * time.Second)
	fmt.Println("It's time to stop all sub goroutines!")
	cancel()

	//Just for test whether sub goroutines exit or not
	time.Sleep(5 * time.Second)
}
Copy the code

The coroutine HandelRequest() used to process a request creates two more coroutines: WriteRedis() and WriteDatabase(), the main coroutine creates the context and passes the context between child coroutines. The main coroutine cancels all child coroutines when appropriate.

4. timerCtx–WithTimerout()

Structure definition

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

	deadline time.Time
}
Copy the code

TimerCtx adds the deadline to indicate the final time of automatic cancel on the basis of cancelCtx, while timer is a timer that triggers automatic cancel.

From this, WithDeadline() and WithTimeout() are derived. The two types implement the same principle, but in different contexts:

  • Deadline: Specifies the deadline, for example, the context will automatically end at 2018.10.20 00:00:00
  • Timeout: Specifies the maximum lifetime. For example, the context will end 30 seconds later.

For the interface, timerCtx implements the Deadline() and cancel() methods in addition to cancelCtx, where cancel() is overridden.

Cancel () interface implementation

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()
	ifc.timer ! =nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}
Copy the code

The cancel() method basically inherits cancelCtx, except that the timer is turned off.

When timerCtx is turned off, timerctx. cancelctx. err will store the reason for turning off:

  • If the deadline is closed manually, the closing reason is the same as cancelCtx.
  • If the event is automatically closed when the deadline comes, the cause is: “Context Deadline exceeded”.

The WithDeadline() method

The WithDeadline() method is implemented as follows:

  • Initialize a timerCtx instance
  • Add the timerCtx instance to the children of its parent (if the parent can also be cancelled)
  • Start the timer. When the timer expires, the local context is automatically cancelled
  • Return the timerCtx instance and cancel() method

That is, context of type timerCtx not only supports manual cancel, but also automatically cancels when the timer arrives.

Its implementation source code is as follows:

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(a) { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func(a) {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func(a) { c.cancel(true, Canceled) }
}
Copy the code

The WithTimeout() method

WithTimeout() actually calls WithDeadline, which is implemented in the same way.

The code should be very clear:

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

Typical Application Cases

The following example uses WithTimeout() to get a context and pass it in its subcoroutine:

package main

import (
	"fmt"
	"time"
	"context"
)

func HandelRequest(ctx context.Context) {
	go WriteRedis(ctx)
	go WriteDatabase(ctx)
	for {
		select {
		case <-ctx.Done():
			fmt.Println("HandelRequest Done.")
			return
		default:
			fmt.Println("HandelRequest running")
			time.Sleep(2 * time.Second)
		}
	}
}

func WriteRedis(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("WriteRedis Done.")
			return
		default:
			fmt.Println("WriteRedis running")
			time.Sleep(2 * time.Second)
		}
	}
}

func WriteDatabase(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("WriteDatabase Done.")
			return
		default:
			fmt.Println("WriteDatabase running")
			time.Sleep(2 * time.Second)
		}
	}
}

func main(a) {
	ctx, _ := context.WithTimeout(context.Background(), 5 * time.Second)
	go HandelRequest(ctx)

	time.Sleep(10 * time.Second)
}
Copy the code

The main coroutine creates a context timed out for 10 seconds and passes it to the child coroutine. The context is automatically closed for 5 seconds.

5. valueCtx–WithValue()

Structure definition

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

ValueCtx simply adds a key-value pair to the Context to pass some data between the levels of the coroutine. Since valueCtx doesn’t need either cancel or deadline, you just need to implement the Value() interface.

Value() interface implementation

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

Valuectx. key and valuectx.val represent their key and value values, respectively. The implementation is simple, but there is one detail that needs to be noted: if the current context cannot find the key, it will look up the parent node, and if it cannot find the key, it will return interface{}. In other words, the parent value can be queried through the child context.

The WithValue() method

The source code is as follows:

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

Typical Application Cases

package main

import (
	"fmt"
	"time"
	"context"
)

func HandelRequest(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("HandelRequest Done.")
			return
		default:
			fmt.Println("HandelRequest running, parameter: ", ctx.Value("parameter"))
			time.Sleep(2 * time.Second)
		}
	}
}

func main(a) {
	ctx := context.WithValue(context.Background(), "parameter"."1")
	go HandelRequest(ctx)

	time.Sleep(10 * time.Second)
}
Copy the code

In the previous example, we used the WithValue() method to get a context, specifying a parent context, key, and value. The context is then passed to HandelRequest, a child coroutine that reads the key-value of the context.

Note: In this case the neutron coroutine cannot terminate automatically because context is cancle unsupported, meaning < -ctx.done () will never return. If you want to return, you specify a context that can cancel as the parent when you create the context, and use the parent’s cancel() to terminate the entire context at an appropriate time.

conclusion

  • Context is simply an interface definition that can be derived from different Context types depending on the implementation;
  • CancelCtx implements the Context interface, creating an instance of cancelCtx with WithCancel();
  • TimerCtx implements the Context interface, creating timerCtx instances with WithDeadline() and WithTimeout().
  • ValueCtx implements the Context interface, creating valueCtx instances with WithValue();
  • Three context instances can parent each other, which can be combined into different application forms;
  • It is recommended that the Context be passed as an argument
  • Function methods that take Context as an argument should take Context as their first argument.
  • When passing Context to a function method, don’t pass nil, if you don’t know what to pass, use context.todo ()
  • The Context’s value-related methods should pass the necessary data for the request field and should not be used to pass optional parameters
  • Context is thread-safe and can be passed safely across multiple Goroutines