Graphical context of Go language to understand the core implementation source code of the programming language

foundation

Some designs in thread-based programming languages

ThreadGroup

image.pngThreadGroup is a commonly used concept in programming languages ​​based on thread concurrency. When a thread derives a child thread, it is usually added to the thread group of the parent thread (if the thread group is not specified), and finally a group can be controlled by ThreadGroup. Thread exit and other operations, and then in the go language goroutine does not have a clear parent/children relationship. If you want to exit all goroutines on the current call chain, you need to use context

ThreadLocal

In thread-based programming languages, some thread-local storage can usually be performed based on ThreadLocal. In essence, key/value storage is performed through a Map, and there is no ThreadLocal design in go. In addition to passing through parameters, context information can also be passed through context.

Typical application scenarios of context

Scenes accomplish principle
contextual information transfer WithValue The key-value pair is saved through an internal key/value attribute, which cannot be modified, and can only be replaced by overwriting.
opt-out notice WithCancel Common exit notification by listening to the notification channel

Recursive acquisition of context data

image.pngBecause map is not used for data storage in the context of go, the actual acquisition is to recurse upward layer by layer from the current layer until a matching key is found.

In fact, we are analogous to ThreadGroup, because goroutine itself does not have the concept of superior and inferior, but in fact, we can realize the parent-child relationship of passing data through context. We can set context data in a goroutine and then pass it to the derived goroutine.

Cancellation notice

image.pngSince the parent/child relationship of parent/child is constructed through context, the context will register itself with the parent during the implementation process. When we cancel the goroutine of a parent, it will actually recursively cancel the done chan of its own child context. So that all goroutines listening to cancel in the entire call chain exit

What if the done chan of a child context is initialized? Then how to notify closed, then directly give you a channel that has been closed by closedchan, is that enough?

with timeout context

image.pngIf we want to implement a timeout control, through the parent/child mechanism of the context above, we only need to start a timer, and then when the timeout expires, we can directly cancel the current context, and we can monitor the current and lower layers. The exit of the goroutine of context.Done()

Background与TODO

image.pngBackgroud is actually very easy to understand literally. In fact, a context object is constructed as a root object, which is essentially a shared global variable. Usually in some system processing, we can use this object as the root object and create a new context. is constructed for contextual data transfer and unified exit control

What about TODO? Usually we set up a lot of todo lists for ourselves. In fact, the same is true here. Although we have built a lot of todo lists, most people can't do anything. Will be used. For example, you will neither monitor the exit nor get data from it. TODO is the same as Background, and it also returns a global variable behind it.

Immutability

Usually we use context to transfer data in a context, such as the processing of an http request request, but if this request is processed, its context will lose its meaning, and we should not continue to reuse a context in the future, if it times out before or canceled, its status will not change

source code implementation

context interface

type Context interface {
    // Deadline返回一个到期的timer定时器,以及当前是否以及到期
    Deadline() (deadline time.Time, ok bool)

    // Done在当前上下文完成后返回一个关闭的通道,代表当前context应该被取消,以便goroutine进行清理工作
    // WithCancel:负责在cancel被调用的时候关闭Done
    // WithDeadline: 负责在最后其期限过期时关闭Done
    // WithTimeout:负责超时后关闭done
    Done() <-chan struct{}

    // 如果Done通道没有被关闭则返回nil
    // 否则则会返回一个具体的错误
    // Canceled 被取消
    // DeadlineExceeded 过期
    Err() error
	// 返回对应key的value
    Value(key interface{}) interface{}
}

emptyCtx

emptyCtx is a context implementation that will not be cancelled, has no expiration time, no value, and will not return an error. It is mainly used as context.Background() and context.TODO() to return this root context or a context that does nothing.

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*emptyCtx) Done() <-chan struct{} {
    return nil
}

func (*emptyCtx) Err() error {
    return nil
}

func (*emptyCtx) Value(key interface{}) interface{} {
    return nil
}
func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

The more interesting implementation is the String method of emptyCtx, which can return the specific type of the current context, such as Background or TODO, because background and todo are two global variables, and the corresponding type is judged by taking its address.

cancelCtx

image.png

structure

A Context object is embedded in the cancelCtx structure, that is, its parent context. At the same time, the interface of all contexts that can be canceled is stored internally through children. When the current context is canceled, it is only necessary to call the context of all canceler interfaces. Implements cancellation of the current call chain

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
}

Done

The Done operation returns the current chan to notify the goroutine to exit


func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil {
        c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
}

cancel

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    // context一旦被某个操作操作触发取消后,就不会在进行任何状态的修改
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    if c.done == nil {
        c.done = closedchan
    } else {
        // close当前chan
        close(c.done)
    }
    // 调用所有children取消
    for child := range c.children {
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    // 是否需要从parent context中移除,如果是当前context的取消操作,则需要进行该操作
    // 否则,则上层context会主动进行child的移除工作
    if removeFromParent {
        removeChild(c.Context, c)
    }
}

timerCtx

image.pngtimerCtx is mainly used to implement two context implementations, WithDeadline and WithTimer. It inherits the cancelCtx interface and also includes a timer.Timer timer and a deadline termination implementation.

2.4.1 Structure

timerCtx

type timerCtx struct {
    cancelCtx
    timer *time.Timer // timer定时器

    deadline time.Time //终止时间
}

Cancellation method

The cancellation method is very simple. First, the cancellation process of cancelCtx is performed, and then the Stop operation of its own timer is performed, so that the cancellation can be realized.

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()
    if c.timer != nil {
        c.timer.Stop() // 停止定时器
        c.timer = nil
    }
    c.mu.Unlock()
}

valueCtx

The value is stored internally through a key/value. If the current context does not contain a value, it will recurse layer by layer.

type valueCtx struct {
    Context
    key, val interface{}
}

func (c *valueCtx) String() string {
    return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

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

propagateCancel

Design goals

The main design goal of propagateCancel is to cancel the child context when the parent context is canceled. There are two modes: 1. When the parent cancels, notify the child to cancel the cancel 2. When the parent cancels, call the child's layer-by-layer recursive cancellation

parentCancelCtx

The context can be arbitrarily nested to form an N-layer tree-structured context. Combining the above two modes, when the parent can be found to be either cancelCtx or timerCtx, the second mode is adopted, and the parent calls the cancel of the child Complete the exit of the entire call chain, otherwise use the first mode to monitor Done

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
	for {
		switch c := parent.(type) {
		case *cancelCtx:
			return c, true	// 找到最近支持cancel的parent,由parent进行取消操作的调用
		case *timerCtx:
			return &c.cancelCtx, true // 找到最近支持cancel的parent,由parent进行取消操作的调用
		case *valueCtx:
			parent = c.Context // 递归
		default:
			return nil, false
		}
	}
}

core implementation

func propagateCancel(parent Context, child canceler) {
	if parent.Done() == nil {
		return // parent is never canceled
	}
	if p, ok := parentCancelCtx(parent); ok {
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
            // 如果发现parent已经取消就直接进行取消
			child.cancel(false, p.err)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
            // 否则加入parent的children map中
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
		go func() {
			select {
			case <-parent.Done():
                // 监听parent DOne完成, 此处也不会向parent进行注册
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

WithDeadline

With the above basic learning WithDeadline, it is much simpler. WithDeadline will set a deadline, and you can calculate how long you need to wait for cancellation through the current time.

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,
	}
    // 监听parent的取消,或者向parent注册自身
	propagateCancel(parent, c)
	dur := time.Until(d)
	if dur <= 0 {
        // 已经过期
		c.cancel(true, DeadlineExceeded) // deadline has already passed
		return c, func() { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
            // 构建一个timer定时器,到期后自动调用cancel取消
			c.cancel(true, DeadlineExceeded)
		})
	}
    // 返回取消函数
	return c, func() { c.cancel(true, Canceled) }
}

Backgroup 与 EVERYTHING

In the invocation of many underlying middleware, information is transmitted through context. The most commonly used ones are Backgroup and Todo. Although they are implemented based on emptyCtx, Backgroup is more inclined to be used as a parent context for subsequent call chain contexts. root is used, and TODO usually indicates that no subsequent operations will be performed, just because the parameters need to be passed for use

Original link http://www.sreguide.com/go/context.html WeChat account: baxiaoshi2020 Follow the announcement number to read more source code analysis articles 21 days greenhouseMore articles follow www.sreguide.com This article is published by OpenWrite , a multi-blog platform

{{o.name}}
{{m.name}}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=324034189&siteId=291194637