context内部如何实现

Context使用场景:

  • 上下文信息传递
  • 控制子 goroutine 的运行
  • 超时控制的方法调用
  • 可以取消的方法调用

context包内部如何实现的?

  • 答案:context是 Go 语言在 1.7 版本中引入标准库的接口。context主要用于父子任务之间的同步取消信号,本质上是一种协程调度的方式。另外在使用context时有两点值得注意:上游任务仅仅使用context通知下游任务不再需要,但不会直接干涉和中断下游任务的执行,由下游任务自行决定后续的处理操作,也就是说context的取消操作是无侵入的;context是线程安全的,因为context本身是不可变的(immutable),因此可以放心地在多个协程中传递使用。

  • 方法:
    Deadline — 返回 context.Context 被取消的时间,也就是完成工作的截止日期;
    Done — 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;
    Err — 返回 context.Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值;
    如果 context.Context 被取消,会返回 Canceled 错误;
    如果 context.Context 超时,会返回 DeadlineExceeded 错误;
    Value — 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;

  • 设计原理
    在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。Go 服务的每一个请求都是通过单独的 Goroutine 处理的,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。
    (context树结构)
    在这里插入图片描述
    每一个 context.Context 都会从最顶层的 Goroutine 一层一层传递到最下层。context.Context 可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。

    如果不使用 context,当最上层的 Goroutine 因为某些原因执行失败时,下层的 Goroutine 由于没有接收到这个信号所以会继续工作;但是当我们正确地使用 context.Context 时,就可以在下层及时停掉无用的工作以减少额外资源的消耗。

    多个 Goroutine 同时订阅 ctx.Done() 管道中的消息,一旦接收到取消信号就立刻停止当前正在执行的工作。

  • 默认上下文

    context 包中最常用的方法还是 context.Background、 context.TODO,这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用。

context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生出来;
context.TODO 应该仅在不确定应该使用哪种上下文时使用

  • 取消信号

    context.WithCancel 函数能够从 context.Context 中衍生出一个新的子上下文并返回用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号。

Context 子树的取消

//  context.WithCancel 函数的实现
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}
func propagateCancel(parent Context, child canceler) {
    done := parent.Done()
    if done == nil {
        return // 父上下文不会触发取消信号
    }
    select {
    case <-done:
        child.cancel(false, parent.Err()) // 父上下文已经被取消
        return
    default:
    }
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            child.cancel(false, p.err)
        } else {
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}
  • context.newCancelCtx 将传入的上下文包装成私有结构体 context.cancelCtx;

  • context.propagateCancel 会构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消:

  • propagateCancel

    propagateCancel 可能出现的情况:

1> 当 parent.Done() == nil,也就是 parent 不会触发取消事件时,当前函数会直接返回;

2> 当 child 的继承链包含可以取消的上下文时,会判断 parent 是否已经触发了取消信号;

  • 如果已经被取消,child 会立刻被取消;
  • 如果没有被取消,child 会被加入 parent 的 children 列表中,等待 parent 释放取消信号;

3> 当父上下文是开发者自定义的类型、实现了 context.Context 接口并在 Done() 方法中返回了非空的管道时;

  • 运行一个新的 Goroutine 同时监听 parent.Done() 和 child.Done() 两个 Channel;
  • 在 parent.Done() 关闭时调用 child.cancel 取消子上下文;
    context.propagateCancel 的作用是在 parent 和 child 之间同步取消和结束的信号,保证在 parent 被取消时,child 也会收到对应的信号,不会出现状态不一致的情况。

context.cancelCtx 实现的几个接口方法也没有太多值得分析的地方,该结构体最重要的方法是 context.cancelCtx.cancel,该方法会关闭上下文
中的 Channel 并向所有的子上下文同步取消信号

// cancel 取消函数
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return
    }
    c.err = err
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }
    for child := range c.children {
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()
    if removeFromParent {
        removeChild(c.Context, c)
    }
}

context 包中的另外两个函数 context.WithDeadline 和 context.WithTimeout 也都能创建可以被取消的计时器上下文 context.timerCtx

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 cur, ok := parent.Deadline(); ok && cur.Before(d) {
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    propagateCancel(parent, c)
    dur := time.Until(d)
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded) // 已经过了截止日期
        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) }
}

context.WithDeadline 在创建 context.timerCtx 的过程中判断了父上下文的截止日期与当前日期,并通过 time.AfterFunc 创建定时器,当时间超过了截止日期后会调用 context.timerCtx.cancel 同步取消信号。
context.timerCtx 内部不仅通过嵌入 context.cancelCtx 结构体继承了相关的变量和方法,还通过持有的定时器 timer 和截止时间 deadline 实现了定时取消的功能

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.
    deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
    c.cancelCtx.cancel(false, err)
    if removeFromParent {
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}

context.timerCtx.cancel 方法不仅调用了 context.cancelCtx.cancel,还会停止持有的定时器减少不必要的资源浪费。

传值方法

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}
 type valueCtx struct {
    Context
    key, val interface{}
}
 func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

context.valueCtx 结构体会将除了 Value 之外的 Err、Deadline 等方法代理到父上下文中,它只会响应 context.valueCtx.Value 方法,该方法的实现也很简单 。
如果 context.valueCtx 中存储的键值对与 context.valueCtx.Value 方法中传入的参数不匹配,就会从父上下文中查找该键对应的值直到某个父上下文中返回 nil 或者查找到对应的值。

go语言代码示例:

package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	//携带参数
	//ctx := context.WithValue(context.Background(), "name", "ten_s")
	//ctx, cancelFunc := context.WithCancel(ctx)

	//设置截止日期
	//ctx, cancelFunc := context.WithDeadline(context.Background(), time.Now().Add(time.Second*5))
	//设置超时时间
	ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*5)
	message := make(chan int)
	go son(ctx, message)

	for i := 0; i < 10; i++ {
		message <- i
	}

	time.Sleep(time.Second)
	defer cancelFunc()
	fmt.Println("主进程结束了")

}

func son(ctx context.Context, message chan int) {
	t := time.Tick(time.Second) // 创建一个每秒发送一次的 ticker

	//ctx_t, clears := context.WithCancel(ctx)
	//go son(ctx_t, message)
	//defer clears()
	for _ = range t {
		select {
		case m := <-message:
			fmt.Printf("接收到值:%d\n", m)

		case <-ctx.Done():
			fmt.Println("子进程结束了", ctx.Value("name"))
			return
		}
	}
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值