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:
- If the parent node also supports cancel, which means that the parent node must have children members, then add the new context to children.
- If the parent does not support cancel, continue searching until you find a node that supports cancel and add the new context to children.
- 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