NOTE

2.1 context

Goroutine context, deadlines, cancellation signals, request-scoped data, Context/canceler implementations, WithCancel/WithTimeout/WithValue, and references.

GoCreated Updated 2 min readhistorical

This is a historical learning note and may contain outdated or incomplete understanding.

1. context

The context of a goroutine. It passes context information between goroutines, including cancellation signals, timeout duration, deadlines, k-v values, etc. It is one way to control concurrency; another is sync.WaitGroup.md.

2. Usage

2.1. deadlines: timeout cancellation

func TestContext1(t *testing.T) {
	// Set a context that times out after one second
	d := time.Now().Add(1 * time.Second)
	ctx, cancel := context.WithDeadline(context.Background(), d)

	// Even though ctx will be expired, it is good practice to call its
	// cancelation function in any case. Failure to do so may keep the
	// context and its parent alive longer than necessary.
	defer cancel()

	select {
	case <-time.After(2 * time.Second):
		fmt.Println("timeout after 2s")
	case <-ctx.Done():
		// Timeout after 1s
		fmt.Println(ctx.Err())
	}
}

// Output
context deadline exceeded

2.2. cancellation signals: control termination of multiple goroutines

  • A channel can only control one goroutine.
func TestChannel1(t *testing.T) {
	stop := make(chan bool)

	go func() {
		for {
			select {
			case <-stop:
				fmt.Println("monitor exits, stopped...")
				return
			default:
				fmt.Println("goroutine keeps monitoring...")
				time.Sleep(2 * time.Second)
			}
		}
	}()

	// Notify the goroutine to stop after 10s
	time.Sleep(10 * time.Second)
	fmt.Println("notify goroutine to stop monitoring")
	stop<- true
	time.Sleep(5 * time.Second)
}

// Output
goroutine keeps monitoring...
goroutine keeps monitoring...
goroutine keeps monitoring...
goroutine keeps monitoring...
goroutine keeps monitoring...
notify goroutine to stop monitoring
monitor exits, stopped...
  • A context can control multiple goroutines.
func TestContext2(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	go watch(ctx, "[monitor 1]")
	go watch(ctx, "[monitor 2]")
	go watch(ctx, "[monitor 3]")

	// Call cancel after 10s to notify all goroutines to cancel
	time.Sleep(10 * time.Second)
	fmt.Println("notify goroutines to stop monitoring")
	cancel()
	// To check whether monitoring has stopped, no monitoring output means it has stopped
	time.Sleep(5 * time.Second)
}

func watch(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println(name, "monitor exits, stopped...", ctx.Err())
			return
		default:
			fmt.Println(name, "goroutine keeps monitoring...")
			time.Sleep(2 * time.Second)
		}
	}
}

// Output
[monitor 3] goroutine keeps monitoring...
[monitor 1] goroutine keeps monitoring...
[monitor 2] goroutine keeps monitoring...
[monitor 2] goroutine keeps monitoring...
[monitor 1] goroutine keeps monitoring...
[monitor 3] goroutine keeps monitoring...
[monitor 3] goroutine keeps monitoring...
[monitor 1] goroutine keeps monitoring...
[monitor 2] goroutine keeps monitoring...
[monitor 3] goroutine keeps monitoring...
[monitor 1] goroutine keeps monitoring...
[monitor 2] goroutine keeps monitoring...
[monitor 2] goroutine keeps monitoring...
[monitor 1] goroutine keeps monitoring...
[monitor 3] goroutine keeps monitoring...
notify goroutines to stop monitoring
[monitor 2] monitor exits, stopped... context canceled
[monitor 3] monitor exits, stopped... context canceled
[monitor 1] monitor exits, stopped... context canceled

2.3. request-scoped data: similar to ThreadLocal

func TestContext3(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	// Attach a value
	valueCtx := context.WithValue(ctx, key, "[monitor 1]")
	go watch2(valueCtx)
	// Call cancel after 10s to notify all goroutines to cancel
	time.Sleep(10 * time.Second)
	fmt.Println("notify goroutines to stop monitoring")
	cancel()
	// To check whether monitoring has stopped, no monitoring output means it has stopped
	time.Sleep(5 * time.Second)
}

func watch2(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			// Get the value
			fmt.Println(ctx.Value(key), "monitor exits, stopped...")
			return
		default:
			// Get the value
			fmt.Println(ctx.Value(key), "goroutine monitoring...")
			time.Sleep(2 * time.Second)
		}
	}
}
// Output
[monitor 1] goroutine monitoring...
[monitor 1] goroutine monitoring...
[monitor 1] goroutine monitoring...
[monitor 1] goroutine monitoring...
[monitor 1] goroutine monitoring...
notify goroutines to stop monitoring
[monitor 1] monitor exits, stopped...

3. Internals

3.1. Class Diagram

3.2. Data Structures

3.2.1. Context Interface

All Context implementations implement this interface.

type Context interface {
	// When the context is canceled or reaches its deadline, return a closed channel
	// This is a receive-only channel. Therefore, a child goroutine cannot read anything from this channel unless it is closed. This is exactly what is used here: after a child goroutine reads a value (the zero value) from the channel, it can do cleanup work and exit as soon as possible.
	Done() <-chan struct{}

	// After channel Done is closed, return the reason the context was canceled, such as cancellation or timeout.
	Err() error

	// Return whether the context will be canceled and its automatic cancellation time (deadline)
	Deadline() (deadline time.Time, ok bool)

	// Get the value corresponding to key
	Value(key interface{}) interface{}
}
3.2.1.1. emptyCtx

The default implementation of the Context interface.

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
}

This emptyCtx is used to define background and todo. background is generally used in the main function, while todo is generally used as a placeholder.

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

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}
3.2.1.2. valueCtx
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)
}

3.2.2. canceler Interface

type canceler interface {
	cancel(removeFromParent bool, err error)
	Done() <-chan struct{}
}

If a Context implements the canceler interface, it is cancelable, such as *cancelCtx and *timerCtx in the diagram above.

3.2.2.1. cancelCtx

Implements both the Context and canceler interfaces.

type cancelCtx struct {
    // Implements Context
	Context

	// Protect the fields below
	mu       sync.Mutex
	done     chan struct{}
	children map[canceler]struct{}
	err      error
}

func (c *cancelCtx) Done() <-chan struct{} {
	c.mu.Lock()
	// Lazy creation: only create it when Done() is called for the first time
	if c.done == nil {
		c.done = make(chan struct{})
	}
	d := c.done
	c.mu.Unlock()
	// Return the channel directly. Callers generally use it with select. Once it is closed, the zero value can be read
	return d
}

// When canceling, the first parameter determines whether this node should remove itself from its parent. The second parameter is a fixed cancellation error type
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    // err must be provided
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	// Already canceled by another goroutine
	if c.err != nil {
		c.mu.Unlock()
		return
	}
	// Assign the err field
	c.err = err
	// Close the channel to notify other goroutines
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done)
	}

	// Traverse all child nodes
	for child := range c.children {
	    // Recursively cancel all child nodes
		child.cancel(false, err)
	}
	// Clear the child nodes
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
	    // Remove itself from the parent node
		removeChild(c.Context, c)
	}
}

func removeChild(parent Context, child canceler) {
	p, ok := parentCancelCtx(parent)
	if !ok {
		return
	}
	p.mu.Lock()
	if p.children != nil {
	    // Delete this node from all child nodes of the parent
		delete(p.children, child)
	}
	p.mu.Unlock()
}

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	for {
	// Only three Context types are recognized here: *cancelCtx, *timerCtx, and *valueCtx. If Context is embedded in another type, it cannot be recognized
		switch c := parent.(type) {
		case *cancelCtx:
			return c, true
		case *timerCtx:
			return &c.cancelCtx, true
		case *valueCtx:
			parent = c.Context
		default:
			return nil, false
		}
	}
}

3.2.2.2. timerCtx
type timerCtx struct {
	cancelCtx
	// Based on cancelCtx, with only one additional time.Timer and one deadline
	timer *time.Timer
	deadline time.Time
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
	// Directly call cancelCtx's cancel method
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		// Remove the child node from the parent
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
	if c.timer != nil {
		// Stop the timer so that it will not cancel again when the deadline arrives
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

3.3. Methods

3.3.1. WithCancel

Creates a cancelCtx.

var Canceled = errors.New("context canceled")

// Pass in a parent Context (usually a background context as the root node)
// Return a new context and cancelFunc. When cancelFunc is called, or the parent node's CancelFunc is called, this context will be canceled
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	// Attach this node to the parent node
	propagateCancel(parent, &c)
	// Eventually call cancel
	return &c, func() { c.cancel(true, Canceled) }
}

func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

func propagateCancel(parent Context, child canceler) {
	// The parent node is an empty node
	if parent.Done() == nil {
		return // parent is never canceled
	}
	// Find a cancelable parent context
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// The parent has already been canceled, so this node (child) must also be canceled
			child.cancel(false, p.err)
		} else {
			// The parent has not been canceled
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			// "Attach" to the parent
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		// If no cancelable parent context was found, start a new goroutine to monitor the parent or child cancellation signal
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

3.3.2. WithTimeout

Creates a timerCtx.

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

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
    // If the parent context's deadline is earlier than the specified time, directly construct a cancelable context.
	if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
		// The reason is that once the parent times out, it automatically calls cancel, and the child will also be canceled.
		// Therefore there is no need to separately handle the child timer automatically calling cancel after its own time expires.
		return WithCancel(parent)
	}

	// Construct timerCtx
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  deadline,
	}
	// Attach to the parent
	propagateCancel(parent, c)

	// Calculate the time from now until the deadline
	d := time.Until(deadline)
	if d <= 0 {
		// Cancel directly
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(true, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		// After d time, timer automatically calls cancel. Automatic cancellation
		c.timer = time.AfterFunc(d, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

var DeadlineExceeded error = deadlineExceededError{}

type deadlineExceededError struct{}

func (deadlineExceededError) Error() string   { return "context deadline exceeded" }

3.3.3. WithValue

Creates a valueCtx.

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

Calling it multiple times constructs a tree.

When retrieving a value, first look in the current node; if it is not found, look in the parent node.

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

4. context canceled vs context deadline exceeded

Assume a context has a 1s timeout. Calling context.Cancel() triggers the context canceled error (active cancellation). If more than 1s passes, it triggers the context deadline exceeded error (passive cancellation).

5. Summary

  • Thread-safe.
  • Context can be used for deadlines, cancellation signals, and passing data in other request scopes.
  • WithCancel, WithDeadline, and WithTimeout accept a parent context and return a child context plus a CancelFunc. Calling CancelFunc cancels the child and its children, and removes the reference from the parent to this child. If it is not called, the child is canceled only when the parent is canceled.
  • Do not put Context in a struct. Share it as the first parameter of a function. When the correct context is uncertain, do not pass nil; use context.TODO.
  • There are three main methods:
    • func WithCancel(parent Context) (ctx Context, cancel CancelFunc): calling CancelFunc can release resources held by the context; otherwise wait for the parent to cancel it.
    • func WithDeadline(parent Context, d time.Time) (Context, CancelFunc): adds a timeout; resources are automatically released when time d is reached.
    • func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) is WithDeadline(parent, time.Now().Add(timeout)).
  • func WithValue(parent Context, key, val interface{}) Context: key must be comparable and cannot be a built-in type.
  • In simple terms, ctx is a linked list, with O(N) efficiency.

6. References

Discussion

Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub