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:
Deadline
Return bind currentcontext
The deadline at which tasks are cancelled; Returns if no deadline is setok == false
.Done
When binding the currentcontext
A closed task is returned when the task is canceledchannel
; If the currentcontext
Will not be cancelled and will returnnil
.Err
ifDone
The returnedchannel
Not closed, will returnnil
; ifDone
The returnedchannel
Closed. A non-empty value is returned indicating the reason the task ended. If it iscontext
Been canceled,Err
Will returnCanceled
; If it iscontext
Timeout,Err
Will returnDeadlineExceeded
.Value
returncontext
Store key value pairs in the currentkey
Corresponding 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.
- if
parent.Done()
returnnil
, indicating that there is no cancelable path above the parent nodecontext
, there is no need to deal with; - If the
context
We found it on the chaincancelCtx
Type 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’schildren
List. - Otherwise, open a coroutine and listen
parent.Done()
andchild.Done()
Once,parent.Done()
The returnedchannel
Shut down, that is,context
An ancestor node in a chaincontext
Is cancelled, will be currentcontext
Also 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
- If the parent node
parent
There is an expiration time and the expiration time is earlier than the given timed
, then the newly created child nodecontext
You do not need to set the expiration timeWithCancel
Create a cancelablecontext
Can; - Otherwise, use
parent
And expiration timed
Create a scheduled canceltimerCtx
And create a new onecontext
And can be cancelledcontext
The ancestor node is disassociated, and then the current time is determined to expired
The length of thedur
:
- if
dur
If the value is less than 0, the new one is cancelledtimerCtx
, the reason forDeadlineExceeded
; - Otherwise, it is a new one
timerCtx
Set 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