Golang source analysis series – Context package

The Context package

Context.Context is the Go language interface introduced to the standard library in version 1.7. Importing the Context package has two main functions

  1. Golang has no thread local variable, and there is no standard solution for passing parameters between different Goroutines
  2. Control management of different GO routines (start, cancel, timeout, etc.)

So let’s look at how golang’s context package solves these two problems first let’s look at the definition of the context interface, okay

type Context interface {
    // Return context, timeout, if any
    Deadline() (deadline time.Time, ok bool)
    // Close the channel for this context
    Done() <-chan struct{}
    // When the return value is not nil, the context has cancelled or timed out
    Err() error
    // The key stored inside
    Value(key interface{}) interface{}}Copy the code

There are four interfaces in total: 1. Deadline() returns the context timeout, Done() returns the channel where the context is closed, which is key to implementing a cross-Go routine task cancellation or stop. 3. Err() When the context is closed or times out, Err records. In other cases Err() returns nil 4.value () returns Value based on the key. This is the key to context as an argument passing vehicle, and its structure will be examined in more detail below

Context: parameter passing

Context as a parameter passing vector, mainly using the Value method, in the Context package corresponding implementation class is ValueCtx, let’s take a look at the source code of ValueCtx

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
    Context
    key, val interface{}}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}
}

...

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

ValueCtx is created using context.withvalue (). The underlying storage structure of ValueCtx is actually a linked list.

package main

import (
    "context"
    "fmt"
)

const (
    KEY1 = "key1"
    KEY2 = "key2"
)

func main(a) {
    // Pass a context with a timeout to tell a blocking function that it
    // should abandon its work after the timeout elapses.
    ctx1 := context.WithValue(context.Background(), KEY1, 100)
    ctx2 := context.WithValue(ctx1, KEY2, 200)
        printVal(ctx2)
    
}

func printVal(ctx context.Context) {
    v1 := ctx.Value(KEY1)
    v2 := ctx.Value(KEY2)
    fmt.Println("v1=", v1) // Output: v1= 100
    fmt.Println("v2=", v2) // output: v2= 200
}
Copy the code

The catch is that valueCtx does not support chained calls

Context: Go routine Control

Context implements go routing control in two main ways, close control and timeout control. In this section we’ll talk about close control, and in the next section we’ll talk about timeout control. Context implements close (cancel) control in a class called cancelCtx

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(a) { c.cancel(true, Canceled) }
}

...

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
}

func (c *cancelCtx) Value(key interface{}) interface{} {
    if key == &cancelCtxKey {
        return c
    }
    return c.Context.Value(key)
}

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
}

func (c *cancelCtx) Err(a) error {
    c.mu.Lock()
    err := c.err
    c.mu.Unlock()
    return err
}

type stringer interface {
    String() string
}

func contextName(c Context) string {
    if s, ok := c.(stringer); ok {
        return s.String()
    }
    return reflectlite.TypeOf(c).String()
}

func (c *cancelCtx) String(a) string {
    return contextName(c.Context) + ".WithCancel"
}

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
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 cancelCtx source code is not too hard to read, mainly propagateCancel code is a bit tricky, I will interpret it in the source code analysis chapter, don’t worry about it for now. Here we pay attention to the main points

  1. There are parent-child relationships between contexts,
  2. WithCancel(parent Context) returns the child Context. If the parent Context is cancelled, the child Context is also cancelled. This is the key to the Go routine

All right, let’s look at the demo

package main

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

func main(a) {
    // Pass a context with a timeout to tell a blocking function that it
    // should abandon its work after the timeout elapses.
    parentCtx, parentCancel := context.WithCancel(context.Background())
    childCtx, childCancel := context.WithCancel(parentCtx)
    
    go parentJob(parentCtx, parentCancel)

    go childJob(childCtx, childCancel)

    time.Sleep(time.Duration(5)*time.Second)
    
}

func parentJob(ctx context.Context, cancel context.CancelFunc) {
    // do someting
    time.Sleep(time.Duration(2)*time.Second)

    fmt.Println("parent job cancled.")
    cancel()
}

func childJob(ctx context.Context, cancel context.CancelFunc) {
    // do someting
    for {
        select {
        case <- ctx.Done():
            fmt.Println("child job cancled because of parent job cancel.")
            return
        default :
            // do some thing, or cancel child job}}}Copy the code

If you want to further understand, it is best to take a look at the package, such as NET, database/ SQL, etc source code, deepen your understanding

Context: timeout control

Context implements timeout control, put down the source code first

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


// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.


    deadline time.Time
}


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


func (c *timerCtx) String(a) string {
    return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
        c.deadline.String() + "[" +
        time.Until(c.deadline).String() + "])"
}


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


// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
/ /}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}
Copy the code

See how to implement timeout control demo

package main

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

func main(a) {
    // Pass a context with a timeout to tell a blocking function that it
    // should abandon its work after the timeout elapses.
    parentCtx, parentCancel := context.WithTimeout(context.Background(), 3*time.Second)
    childCtx, childCancel := context.WithCancel(parentCtx)
    
    go parentJob(parentCtx, parentCancel)

    go childJob(childCtx, childCancel)

    time.Sleep(time.Duration(5)*time.Second)
    
}

func parentJob(ctx context.Context, cancel context.CancelFunc) {
    // do someting
    time.Sleep(time.Duration(2)*time.Second)

    fmt.Println("parent job cancled.")
    cancel()
}

func childJob(ctx context.Context, cancel context.CancelFunc) {
    // do someting
    for {
        select {
        case <- ctx.Done():
            fmt.Println("child job cancled. reason = ", ctx.Err().Error())
            return
        default :
            // do some thing, or cancel child job}}}Copy the code

In the preceding demo, the parentJob timeout period is 3s, and the childJob will be canceled at 3s

Context package source analysis

Now, let’s take a look at the source code of the context package, the above source code is basically put almost, for the convenience of everyone to understand, draw the context class diagramThe above valueCtx code and CancelCtx and timerCtx code have been put in place, and it is not difficult to understand, but we will focus on the more complicated functions

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

    // This code is not required, but you can see the power, because if the parent context has been cancelled, it is returned directly, without locking
    select {
    case <-done:
        // parent is already canceled
        child.cancel(false, parent.Err())
        return
    default:}if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        ifp.err ! =nil {
            // parent has already been canceled
            ParentCancelCtx returns to normal in a multi-coroutine environment, and between p.m.Lock, context may cancel
            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(a) {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}
Copy the code

Ok, next time share the Time pack

Golang community

Knowledge Planet, together golang: t.zsxq.com/nUR723R Blog: blog.17ledong.com/