foundation
Some designs in thread-based programming languages
ThreadGroup
ThreadGroup 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
Because 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
Since 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
If 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
Backgroud 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
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
timerCtx 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
More articles follow www.sreguide.com This article is published by OpenWrite , a multi-blog platform