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
- Golang has no thread local variable, and there is no standard solution for passing parameters between different Goroutines
- 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
- There are parent-child relationships between contexts,
- 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/