013-Golang1.17源码分析之Context

Golang1.17源码分析之Context

Golang1.17 学习笔记013

context 是一种常用额并发控制技术,与 WaitGroup 最大的区别就是 context 对派生的 G 也有控制权。
可以控制多级的 goroutine

context 译为“上下文”,可以控制一组成树状结构的 goroutine,每个 goroutine 都有相同的 context

13.1 实现

源码位置:src/context/context.go:Context

type Context interface {

    // 返回一个deadline和标识是否已设置deadline的bool值,如果没有设置deadline,
    // 则ok == false,此时deadline为一个初始值的time.Time值
    Deadline() (deadline time.Time, ok bool)
    
    //context 关闭时, Done()返回一个被关闭的管道,关闭的管理仍然是可读的,据此goroutine可以收到关闭请求;
    // 当context还未关闭时,Done()返回nil
    Done() <-chan struct{}
    
    // 因deadline关闭:“context deadline exceeded”
    // 因主动关闭: “context canceled”
    // 当 context 还未关闭时,Err()返回nil
    Err() error
    
    // 有一种context,它不是用于控制呈树状分布的goroutine,而是用于在树状分布的goroutine间传递信息。
    // Value()方法就是用于此种类型的context,该方法根据key值查询map中的value
    Value(key interface{}) interface{}
}

13.2 空context

context包中定义了一个空的context,名为emptyCtx,用于context的根节点,
空的context只是简单的实现了Context,本身不包含任何值,仅用于其他context的父节点


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
}

context包中定义了一个公用的emptCtx全局变量,名为background,可以使用context.Background()获取它

var background = new(emptyCtx)
func Background() Context {
    return background
}

context包中实现 Context 接口的 struct,除了 emptyCtx 外,还有 cancelCtx、timerCtx 和 valueCtx三种。
正是基于这三种实例,实现了 WithCancel()、WithDeadline()、WithTimeout() 和 WithValue() 四种 context

13.3 WithCancel

WithCancel()方法作了三件事:

初始化一个cancelCtx实例

将cancelCtx实例添加到其父节点的children中(如果父节点也可以被cancel的话)

返回cancelCtx实例和cancel()方法

源码:

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() { c.cancel(true, Canceled) }
}

CancelCtx 定义如下:

type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of 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
}

此 context 被 cancle 时会把其中的所有 child 都 cancle 掉

Done() 接口实现:Done() 接口只需要返回一个 channel 即可,对于 cancelCtx 来说
只需要返回成员变量 done 即可


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

cancel() 接口实现:

cancel() 内部方法是理解 cancelCtx 的最关键的方法,其作用是关闭自己和其后代,其后代存储 在 cancelCtx.children 的 map 中,其 中key 值即后代对象,value 值并没有意义

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)
	} else {
		close(d)
	}
	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)
	}
}

一个经典的例子:

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() {
    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)
}

13.4 timeCtx

type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

衍生出WithDeadline()和WithTimeout()。实现上这两种类型实现原理
一样,只不过使用语境不一样:

deadline: 指定最后期限,比如 context 将 xxxx.xx.xx xx:xx:xx 之时自动结束

timeout: 指定最长存活时间,比如 context 将 在30s 后结束

WithTimeout() 和 WithDeadline() 实现:核心还是 WithDeadline

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

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() { c.cancel(false, Canceled) }
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if c.err == nil {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

经典示例:

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() {
    ctx, _ := context.WithTimeout(context.Background(), 5 * time.Second)
    go HandelRequest(ctx)
    time.Sleep(10 * time.Second)
}

13.5 valueCtx

valueCtx 只是在 Context 基础上增加了一个 key-value 对,用于在各级协程间传递一些数据

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

value()接口实现:

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

WithValue() 实现:

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}
}

经典使用案例:

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() {
    ctx := context.WithValue(context.Background(), "parameter", "1")
    go HandelRequest(ctx)
    time.Sleep(10 * time.Second)
}

13.6 总结

  1. 不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。

  2. 不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo。

  3. 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。

  4. 同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。


版权声明:本文为weixin_51546892原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。