preface

This article introduces a programming pattern commonly used in Golang concurrent programming: Context. This article will start with why context is needed, take a deeper look at how context is implemented, and learn how to use context.

Why do WE need context

In concurrent programs, preemption or interruption of subsequent operations is often required because of timeouts, cancellations, or exceptions. Anyone familiar with Channels has probably seen this kind of problem solved using the Done Channel. Take this example:

func main(a) {

    messages := make(chan int.10)

    done := make(chan bool)



    defer close(messages)

    // consumer

    go func(a) {

        ticker := time.NewTicker(1 * time.Second)

        for _ = range ticker.C {

            select {

            case <-done:

                fmt.Println("child process interrupt...")

                return

            default:

                fmt.Printf("send message: %d\n", <-messages)

            }

        }

} ()



    // producer

    for i := 0; i < 10; i++ {

        messages <- i

    }

    time.Sleep(5 * time.Second)

    close(done)

    time.Sleep(1 * time.Second)

    fmt.Println("main process exit!")

}

Copy the code

The example above defines a channel done with buffer 0, and the subcoroutine runs a timed task. If the main coroutine needs to send a message at some point to notify the subcoroutine of its abort task exit, it can have the subcoroutine listen on the Done Channel. Once the main coroutine closes the Done Channel, the subcoroutine can be pushed out, fulfilling the need for the main coroutine to notify the subcoroutine. That’s good, but it’s also limited.

If we can add additional information to a simple notification to control cancellation: why it was canceled, or if there is a deadline by which it must be completed, or if there are multiple cancellation options, we need to decide which cancellation option to execute based on the additional information.

Consider the following case: if the main coroutine has multiple tasks 1, 2… M, the main coroutine has timeout control over these tasks; And task 1 has multiple subtasks 1, 2… N, task 1 also has its own timeout control over these subtasks, so these subtasks need to sense both the cancellation signal of the main coroutine and the cancellation signal of task 1.

If we still use the Done Channel usage, we need to define two Done Channels, and the subtasks need to listen on both of them. Well, that actually seems to work. But if you go deeper, and if these subtasks have subtasks, then the way to use the Done Channel can become tedious and confusing.

We need an elegant solution to implement such a mechanism:

  • [Fixed] After the upper level mission is cancelled, all lower level missions are cancelled.
  • If a task at a middle level is cancelled, only the tasks at the lower level of the current task will be cancelled, without affecting the tasks at the upper level and other tasks at the same level.

This is where context comes in handy. Let’s start by looking at the architecture and implementation of context.

What is the context

The context interface

Let’s look at the Context interface structure, which looks pretty simple.

type Context interface {



    Deadline() (deadline time.Time, ok bool)



    Done() <-chan struct{}



    Err() error



    Value(key interface{}) interface{}

}

Copy the code

The Context interface contains four methods:

  • DeadlineReturn bind currentcontextThe deadline at which tasks are cancelled; Returns if no deadline is setok == false.
  • DoneWhen binding the currentcontextA closed task is returned when the task is canceledchannel; If the currentcontextWill not be cancelled and will returnnil.
  • ErrifDoneThe returnedchannelNot closed, will returnnil; ifDoneThe returnedchannelClosed. A non-empty value is returned indicating the reason the task ended. If it iscontextBeen canceled,ErrWill returnCanceled; If it iscontextTimeout,ErrWill returnDeadlineExceeded.
  • ValuereturncontextStore key value pairs in the currentkeyCorresponding value, if there is no corresponding valuekey, the returnnil.

As you can see, the channel returned by the Done method is used to transmit the end signal to preempt and interrupt the current task; The Deadline method indicates whether the current goroutine will be cancelled after some time; And an Err method to explain why goroutine was canceled; Value is used to get additional information specific to the current task tree. And the additional information that context contains how are key-value pairs stored? If you can imagine a tree, each node of the tree might carry a set of key-value pairs, and if you can’t find the value of the key on the current node, you’ll look up the parent node to the root node, as we’ll see later.

Let’s look at the other key things in the Context package.

emptyCtx

EmptyCtx is a variable of type int, but implements the context interface. EmptyCtx has no timeout, cannot be cancelled, and cannot store any additional information, so emptyCtx is used as the root of the context tree.

// An emptyCtx is never canceled, has no values, and has no deadline. It is not

// struct{}, since vars of this type must have distinct addresses.

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

}



func (e *emptyCtx) String(a) string {

    switch e {

    case background:

        return "context.Background"

    case todo:

        return "context.TODO"

    }

    return "unknown empty Context"

}



var (

    background = new(emptyCtx)

    todo       = new(emptyCtx)

)



func Background(a) Context {

    return background

}



func TODO(a) Context {

    return todo

}

Copy the code

Instead of using emptyCtx directly, we use two variables instantiated by emptyCtx, which can be obtained by calling Background and TODO methods, respectively, but which are implementatively the same. So what’s the difference between Background and the context that you get from TODO? Take a look at the official explanation:

// Background returns a non-nil, empty Context. It is never canceled, has no

// values, and has no deadline. It is typically used by the main function,

// initialization, and tests, and as the top-level Context for incoming

// requests.



// TODO returns a non-nil, empty Context. Code should use context.TODO when

// it's unclear which Context to use or it is not yet available (because the

// surrounding function has not yet been extended to accept a Context

// parameter).

Copy the code

Background and TODO are only used in different scenarios: Background is usually used in main functions, initializations, and tests, as a top-level context, that is, we usually create contexts based on Background; TODO is used when you’re not sure what context to use.

Here are the basic context types for two different functions: valueCtx and cancelCtx.

valueCtx

ValueCtx structure
type valueCtx struct {

    Context

    key, val interface{}

}



func (c *valueCtx) Value(key interface{}) interface{} {

    if c.key == key {

        return c.val

    }

    return c.Context.Value(key)

}

Copy the code

ValueCtx uses a variable of type Context to represent the parent node Context, so the current Context inherits all of the parent Context’s information; The valueCtx type also carries a set of key-value pairs, meaning that this context can carry additional information. ValueCtx implements the Value method, which is used to obtain the Value corresponding to the key on the context link. If no key is required on the context link, valueCtx searches for the Value corresponding to the key up the context chain until the root node.

WithValue

WithValue adds key-value pairs to context:

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}

}

Copy the code

Instead of adding a key-value pair to the original context structure, you create a new valueCtx child node with the context as its parent, and add the key-value pair to the child node to form a context chain. The process of getting a value is to search up the tail of the context chain:

cancelCtx

CancelCtx structure
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

}



type canceler interface {

    cancel(removeFromParent bool, err error)

    Done() <-chan struct{}

}

Copy the code

Like valueCtx, cancelCtx has a context variable as its parent; The done variable represents a channel, which is used to pass the close signal; Children represents a map that stores the children of the current context node. Err Stores error information indicating the reason for ending a task.

CancelCtx cancelCtx

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

}



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

    }

    // Set the cancellation reason

    c.err = err

Set a closed channel or close the Done Channel to send a closed signal

    if c.done == nil {

        c.done = closedchan

    } else {

        close(c.done)

    }

    // Cancel the child node context in sequence

    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 {

        // Remove the current context node from the parent node

        removeChild(c.Context, c)

    }

}

Copy the code

A cancelCtx type variable is also a Canceler type because cancelCtx implements the Canceler interface. CancelCtx cancels the context by setting the Done channel to a closed channel or a closed channel, and then cancelling the child context in sequence. The current node is also removed from the parent node if necessary.

WithCancel

The WithCancel function is used to create a cancelable context, which is of type cancelCtx. WithCancel returns a context and a CancelFunc. CancelFunc triggers the cancel operation. Direct look at the source code:

type CancelFunc func(a)



func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
 {

    c := newCancelCtx(parent)

    propagateCancel(parent, &c)

    return &c, func(a) { c.cancel(true, Canceled) }

}



// newCancelCtx returns an initialized cancelCtx.

func newCancelCtx(parent Context) cancelCtx {

    // generate a new child node with parent as context

    return cancelCtx{Context: parent}

}



func propagateCancel(parent Context, child canceler) {

    if parent.Done() == nil {

        // parent.done () returns nil indicating that there is no cancelable context above the parent

        return // parent is never canceled

    }

    // Get the ancestor node whose latest type is cancelCtx

    if p, ok := parentCancelCtx(parent); ok {

        p.mu.Lock()

        ifp.err ! =nil {

            // parent has already been canceled

            child.cancel(false, p.err)

        } else {

            if p.children == nil {

                p.children = make(map[canceler]struct{})

            }

            // Add the current child to the children of the most recent cancelCtx ancestor

            p.children[child] = struct{} {}

        }

        p.mu.Unlock()

    } else {

        go func(a) {

            select {

            case <-parent.Done():

                child.cancel(false, parent.Err())

            case <-child.Done():

            }

} ()

    }

}



func parentCancelCtx(parent Context) (*cancelCtx, bool) {

    for {

        switch c := parent.(type) {

        case *cancelCtx:

            return c, true

        case *timerCtx:

            return &c.cancelCtx, true

        case *valueCtx:

            parent = c.Context

        default:

            return nil.false

        }

    }

}

Copy the code

CancelCtx cancelCtx refers to the cancellation of all cancelCtx in descendant nodes, propagateCancel is used to establish the cancellation logic between the current node and an ancestor node.

  1. ifparent.Done()returnnil, indicating that there is no cancelable path above the parent nodecontext, there is no need to deal with;
  2. If thecontextWe found it on the chaincancelCtxType ancestor node, then determine whether the ancestor node has been canceled, if so, cancel the current node; Otherwise, the current node is added to the ancestor node’schildrenList.
  3. Otherwise, open a coroutine and listenparent.Done()andchild.Done()Once,parent.Done()The returnedchannelShut down, that is,contextAn ancestor node in a chaincontextIs cancelled, will be currentcontextAlso cancelled.

There may be a question here, why the ancestor node and not the parent node? This is because the current context chain might look like this:

The current cancelCtx parent context is not a cancelable context, so children cannot be recorded.

timerCtx

TimerCtx is a cancelCtx based context, which can be cancelled at a certain time.

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) cancel(removeFromParent bool, err error) {

CancelCtx cancels the internal cancelCtx

    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 {

Cancel timer

        c.timer.Stop()

        c.timer = nil

    }

    c.mu.Unlock()

}

Copy the code

CancelCtx is used to cancel timerCtx, and timer and deadline are used to cancel timerCtx. When timerCtx calls the Cancel method, it cancels the internal cancelCtx first, removes itself from the cancelCtx ancestor if necessary, and finally cancels the timer.

WithDeadline

WithDeadline returns a parent-based cancelable context with an expiration deadline no later than d.

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,

    }

    // Create a disassociation relationship between the new context and the ancestors of the cancelable context

    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
  1. If the parent nodeparentThere is an expiration time and the expiration time is earlier than the given timed, then the newly created child nodecontextYou do not need to set the expiration timeWithCancelCreate a cancelablecontextCan;
  2. Otherwise, useparentAnd expiration timedCreate a scheduled canceltimerCtxAnd create a new onecontextAnd can be cancelledcontextThe ancestor node is disassociated, and then the current time is determined to expiredThe length of thedur:
  • ifdurIf the value is less than 0, the new one is cancelledtimerCtx, the reason forDeadlineExceeded;
  • Otherwise, it is a new onetimerCtxSet the timer to cancel the current once the expiration time is reachedtimerCtx.
WithTimeout

Similar to WithDeadline, WithTimeout creates a timed cancellation context, except that WithDeadline receives an expiration time, whereas WithTimeout receives an expiration time relative to the current time:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {

    return WithDeadline(parent, time.Now().Add(timeout))

}

Copy the code

The context of use

Done Channel (context) ¶ Context (context) ¶

func main(a) {

    messages := make(chan int.10)



    // producer

    for i := 0; i < 10; i++ {

        messages <- i

    }



    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)



    // consumer

    go func(ctx context.Context) {

        ticker := time.NewTicker(1 * time.Second)

        for _ = range ticker.C {

            select {

            case <-ctx.Done():

                fmt.Println("child process interrupt...")

                return

            default:

                fmt.Printf("send message: %d\n", <-messages)

            }

        }

    }(ctx)



    defer close(messages)

    defer cancel()



    select {

    case <-ctx.Done():

        time.Sleep(1 * time.Second)

        fmt.Println("main process exit!")

    }

}

Copy the code

In this example, the child thread can simply listen for CTX from the main thread and cancel the task once ctx.done () returns an empty channel. But this example doesn’t show the powerful advantage of context passing cancellation information.

Those of you who have read the net/ HTTP package source code may have noticed that context is used when implementing HTTP Server.

1. When the Server starts the service, it creates a valueCtx, which stores the information about the Server. After that, each connection is established, a coroutine is opened and carries the valueCtx.

func (srv *Server) Serve(l net.Listener) error {



.



    var tempDelay time.Duration     // how long to sleep on accept failure

    baseCtx := context.Background() // base is always background, per Issue 16220

    ctx := context.WithValue(baseCtx, ServerContextKey, srv)

    for {

        rw, e := l.Accept()



.



        tempDelay = 0

        c := srv.newConn(rw)

        c.setState(c.rwc, StateNew) // before Serve can return

        go c.serve(ctx)

    }

}

Copy the code

2. After the connection is established, a valueCtx is created based on the incoming context to store the local address information, and then a cancelCtx is created based on this, and the network request is read from the current connection. Each time a request is read, the cancelCtx will be passed in to pass the cancellation signal. Once the connection is disconnected, a cancel signal is sent to cancel all network requests in progress.

func (c *conn) serve(ctx context.Context) {

    c.remoteAddr = c.rwc.RemoteAddr().String()

    ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr())

.



    ctx, cancelCtx := context.WithCancel(ctx)

    c.cancelCtx = cancelCtx

    defer cancelCtx()



.



    for {

        w, err := c.readRequest(ctx)



.



        serverHandler{c.server}.ServeHTTP(w, w.req)



.

    }

}

Copy the code

3. After the request is read, a new cancelCtx will be created based on the incoming context and set to the current request object req. At the same time, the generated response object cancelCtx saves the current context cancellation method.

func (c *conn) readRequest(ctx context.Context) (w *response, err error) {



.



    req, err := readRequest(c.bufr, keepHostHeader)



.



    ctx, cancelCtx := context.WithCancel(ctx)

    req.ctx = ctx



.



    w = &response{

        conn:          c,

        cancelCtx:     cancelCtx,

        req:           req,

        reqBody:       req.Body,

        handlerHeader: make(Header),

        contentLength: - 1.

        closeNotifyCh: make(chan bool.1),



        // We populate these ahead of time so we're not

        // reading from req.Header after their Handler starts

        // and maybe mutates it (Issue 14940)

        wants10KeepAlive: req.wantsHttp10KeepAlive(),

        wantsClose:       req.wantsClose(),

    }



.

    return w, nil

}

Copy the code

The purpose of this treatment is as follows:

  • Once the request times out, the current request can be interrupted;

  • If an error occurs during the process of constructing a Response, the cancelCtx method of the Response object can be directly called to end the current request.

  • After processing builds the Response, call the cancelCtx method on the Response object to terminate the current request.

In the whole server processing process, a context chain is used to run through the server, Connection, and Request, which not only shares upstream information with downstream tasks, but also realizes that upstream can send cancellation signals to cancel all downstream tasks. The cancellation of downstream tasks does not affect upstream tasks.

conclusion

Context is mainly used for synchronization cancellation signals between parent and child tasks, which is essentially a way of coroutine scheduling. In addition, there are two points worth noting when using context: the upstream task only uses context to notify the downstream task that it is no longer needed, but it will not directly interfere with or interrupt the execution of the downstream task. The downstream task decides the subsequent processing operations by itself, that is to say, the context cancellation operation is non-intrusive. Context is thread-safe because context is immutable and can be safely passed across multiple coroutines.

The resources

1, the Package of the context

2, Go Concurrency Patterns: Context

3. Understanding the context package in golang