Golang context 原理分析

  • 1. 说在前面
  • 2. 场景分析
    • 2.1 链式传递
    • 2.2 主动取消
    • 2.3 任务超时
    • 2.4 数据存储
  • 3. 源码解读
    • 3.1 一个核心数据结构
      • 3.1.1 Context
    • 3.2 四种具体实现
      • 3.2.1 emptyCtx
      • 3.2.2 cancelCtx
      • 3.2.3 timerCtx
      • 3.2.4 valueCtx
    • 3.3 六个核心方法
      • 3.3.1 Background() && TODO()
      • 3.3.2 WithCancel()
      • 3.3.3 WithDeadline()
      • 3.3.4 WithTimeout()
      • 3.3.5 WithValue()
  • 4. 一些思考
    • 思考1:emptyCtx 为什么不是 struct{}类型?
    • 思考2:backgound 和 todo 有什么区别?
    • 思考3:cancelCtx 怎么保证父亲 👨 取消的同时取消儿子 👦?
    • 思考4:valueCtx 可以用于数据存储吗?

1. 说在前面

context 是 golang 中的经典工具,主要在异步场景中用于实现并发协调控制以及对 goroutine 的生命周期管理。除此之外,context 还兼有一定的数据存储能力。本文旨在剖析 context 的核心工作原理。

本文使用到的 Go 版本为 1.18,源码位置 src/context/context.go

2. 场景分析

2.1 链式传递

在 Go 中可以认为协程的组织是一种链式传递,每一个子协程的创建都是基于父协程,但是父协程对子协程的控制则是通过 context 实现;同样的,每一个 context 也都是基于父 context 创建,最终形成链式结构,根 context 就是 emptyCtx。

2.2 主动取消

取消场景,是父协程任务取消的时候,将子协程一并取消。

在下面这个案例中,子协程的任务需要 2s 才能执行完,但是父协程 1s 后执行报错主动 cancel 任务的执行,cancel() 方法会通知子协程一并取消任务的执行。

package mainimport ("context""errors""fmt""time"
)func testCtx(ctx context.Context) {cancelCtx, cancel := context.WithCancel(ctx)defer cancel()go func(ctx context.Context) {cancelCtx, cancel := context.WithCancel(ctx)defer cancel()// do somethingselect {case <-time.After(2 * time.Second):// 假设任务需要 2s 完成fmt.Println("work done")case <-cancelCtx.Done():fmt.Println("work canceled")return}// do something}(cancelCtx)// 模拟执行过程中父协程报错取消任务<-time.After(1 * time.Second)err := errors.New("fake err")if err != nil {cancel()}
}func main() {ctx := context.Background()testCtx(ctx)<-time.After(3 * time.Second)
}

2.3 任务超时

超时场景,是父协程任务超时的时候会触发取消流程,需将子协程一并取消。

在下面这个案例中,子协程的任务需要 2s 才能执行完,但是父协程 1s 后任务超时,开始执行取消任务的流程,通知子协程一并取消任务的执行。

package mainimport ("context""fmt""time"
)func testCtx(ctx context.Context) {timerCtx, cancel := context.WithTimeout(ctx, 1*time.Second)defer cancel()go func(ctx context.Context) {timerCtx, cancel := context.WithTimeout(ctx, 2*time.Second)defer cancel()// do somethingselect {case <-time.After(2 * time.Second):// 假设任务需要 2s 完成fmt.Println("work done")case <-timerCtx.Done():fmt.Println("work canceled")return}// do something}(timerCtx)// 模拟等待子协程退出(偷懒)<-time.After(2 * time.Second)
}func main() {ctx := context.Background()testCtx(ctx)<-time.After(3 * time.Second)
}

2.4 数据存储

context 可以用于数据存储,通常是用于存储一些元数据。

package mainimport ("context""fmt""time"
)func testCtx(ctx context.Context) {valueCtx := context.WithValue(ctx, "name", "father")fmt.Println(valueCtx.Value("name"))go func(ctx context.Context) {valueCtx := context.WithValue(ctx, "name", "child")fmt.Println(valueCtx.Value("name"))}(valueCtx)
}func main() {ctx := context.Background()testCtx(ctx)<-time.After(3 * time.Second)
}

3. 源码解读

3.1 一个核心数据结构

3.1.1 Context

首先 Context 本质是官方提供的一个 interface,实现了该 interface 定义的都被称之为 context。

type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key any) any
}
  • Deadline() 返回 ctx 生命终了时间,如果没有则 ok 为 false
  • Done() 返回一个 channel 用于判断 ctx 是否已经结束
  • Err() 用于 ctx 结束后获取错误信息
  • Value() 获取 ctx 中存入的键值对

3.2 四种具体实现

3.2.1 emptyCtx

官方实现的一个空 ctx 版本,默认都是返回空值,通常是作为所有 ctx 的根。

type emptyCtx intfunc (*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 any) any {return nil
}

3.2.2 cancelCtx

具有取消功能,父 ctx 取消的时候将所有子 ctx 一并取消。

类型定义

type cancelCtx struct {Contextmu       sync.Mutex            // 保护临界资源done     atomic.Value          // chan struct{} 类型,用于判断 ctx 是否取消,第一次调用 cancel() 后 closechildren map[canceler]struct{} // 存储所有的子 ctxerr      error                 // ctx 取消后记录错误信息
}

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{})
}
  • 如果 done 不为空则直接返回
  • 加锁🔒
  • 如果 done 为空则创建,这里也说说明一点:done 是懒加载的,第一次调用 Done() 方法才会创建 done
  • 返回 done
  • 解锁🔒

Value() 方法实现

var cancelCtxKey intfunc (c *cancelCtx) Value(key any) any {if key == &cancelCtxKey {return c}return value(c.Context, key)
}
  • 如果传入的 key 是 cancelCtxKey 则返回自身,这里制定了一种私有协议(外部无法访问 cancelCtxKey),用于后面判断一个父 ctx 是否是一个 cancelCtx 类型
  • 否则调用 value 方法找到 key 对应的 value

Err() 方法实现

func (c *cancelCtx) Err() error {c.mu.Lock()err := c.errc.mu.Unlock()return err
}
  • 加锁🔒保护,返回 err

Deadline() 方法实现

  • 未实现,继承父 Context

3.2.3 timerCtx

具有定时取消的功能,因为是继承自 cancelCtx 所以同样具有主动取消功能

类型定义

type timerCtx struct {cancelCtxtimer *time.Timer  // timerCtx 内部维护的一个定时器deadline time.Time // ctx 的终了时间
}

Done() 方法实现

  • 未实现,继承父 cancelCtx

Value() 方法实现

  • 未实现,继承父 cancelCtx

Err() 方法实现

  • 未实现,继承父 cancelCtx

Deadline() 方法实现

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {return c.deadline, true
}
  • 返回 dealline

3.2.4 valueCtx

具有存储数据的功能,通常是一些元数据信息

类型定义

type valueCtx struct {Contextkey, val any // 记录数据
}

Done() 方法实现

  • 未实现,继承父 Context

Value() 方法实现

func (c *valueCtx) Value(key any) any {if c.key == key {return c.val}return value(c.Context, key)
}func value(c Context, key any) any {for {switch ctx := c.(type) {case *valueCtx:if key == ctx.key {return ctx.val}c = ctx.Contextcase *cancelCtx:if key == &cancelCtxKey {return c}c = ctx.Contextcase *timerCtx:if key == &cancelCtxKey {return &ctx.cancelCtx}c = ctx.Contextcase *emptyCtx:return nildefault:return c.Value(key)}}
}
  • 判断传入的 key 是否等于当前 ctx 的 k,如果相等则返回
  • 否则就从父 ctx 中找,一直到 emptyCtx 找不到就返回 nil

Err() 方法实现

  • 未实现,继承父 Context

Deadline() 方法实现

  • 未实现,继承父 Context

3.3 六个核心方法

3.3.1 Background() && TODO()

用于获取 emptyCtx,本质上没有区别,仅仅是语义上的区别。

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

3.3.2 WithCancel()

函数说明:传入一个父 ctx 返回一个子 ctx 和一个 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) }
}func newCancelCtx(parent Context) cancelCtx {return cancelCtx{Context: parent}
}
  • 如果传入的 parent 是空的,panic
  • 基于 parent 创建 cancelCtx
  • propagateCancel 用于传递 cancel 的特性,用于保证父 ctx 取消的时候,子 ctx 也取消
  • 返回创建的 cancelCtx 和一个闭包函数,闭包函数调用 cancel 取消创建的 ctx
func propagateCancel(parent Context, child canceler) {done := parent.Done()if done == nil {return // parent is never canceled}select {case <-done:// parent is already canceledchild.cancel(false, parent.Err())returndefault:}if p, ok := parentCancelCtx(parent); ok {p.mu.Lock()if p.err != nil {// parent has already been canceledchild.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() {select {case <-parent.Done():child.cancel(false, parent.Err())case <-child.Done():}}()}
}
  • parent 是一个永远不会被取消的 ctx,直接 return
  • 如果 parent 已经被取消了,把当前新创建的 cancelCtx 也取消,并 return
  • 如果 parent 是一个 cancelCtx 类型则将新创建的 cancelCtx 加入到 parent 的 children 中
  • 如果 parent 不是 cancelCtx 类型,则启动一个 goroutine 来保证 parent 取消的时候当前 cancelCtx 也被取消掉
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 = errd, _ := 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 = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c)}
}
  • 前置判断
  • 将 done chan 关闭
  • 遍历 children,将所有的子 ctx 都一并取消掉
  • 如果 parent 是 cancelCtx 类型,需要将当前 ctx 从 parent 的 children 中删除

3.3.3 WithDeadline()

函数说明:传入一个父 ctx,和一个终了时间,返回一个子 ctx 和一个 cancel 函数;timerCtx 继承自 cancelCtx,拥有 cancelCtx 的一切特性

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 passedreturn 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) }
}
  • 如果 parent 也是一个 timerCtx 并且传入的终了时间在 parent 的终了时间之后,那么新创建的 ctx 就没必要拥有定时特性,使用 WithCancel 构造一个 cancelCtx 返回即可
  • 否则创建一个 timerCtx
  • propagateCancel 传递 cancel 的特性
  • 如果 deadline 时间已经过了,直接 cancel 然后 return
  • 创建一个定时任务,定时结束触发 cancel
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()
}
  • 调用 parent 的 cancel 函数
  • 如果 parent 是 cancelCtx 类型,需要将当前 ctx 从 parent 的 children 中删除
  • 定时器 stop

3.3.4 WithTimeout()

仅仅对 WithDeadline 进行了简单封装

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

3.3.5 WithValue()

func WithValue(parent Context, key, val any) 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}
}

4. 一些思考

思考1:emptyCtx 为什么不是 struct{}类型?

struct{} 作为一个空类型并不占用底层存储空间,所以它的多个不同对象有可能会使用相同的地址,无法区分出 background 和 todo 对象。

思考2:backgound 和 todo 有什么区别?

本质没有区别,都是 emptyCtx,更多的是语义上的区别,background 通常作为所有 ctx 链的最顶层。

思考3:cancelCtx 怎么保证父亲 👨 取消的同时取消儿子 👦?

机制 1:cancelCtx 有一个 children 字段记录了所有的子节点,当父节点被取消的时候会给所有子节点来一刀 🔪,依次传递最终将所有子、孙子、孙孙子都刀 🔪 了。父亲取消的时候也会通知爷爷,让爷爷从 children 中删除父亲。

机制 2:如果父亲不是一个 cancelCtx 类型,则不会有 children 属性怎么办?当使用 WithCancel()创建的时候,发现父亲不是 cancelCtx 就会启动一个守护协程判断父亲是否 Done(),如果父亲 over 了,就会干掉儿子并退出;否则儿子先挂了,也会退出。

思考4:valueCtx 可以用于数据存储吗?

valueCtx 不适合视为存储介质,存放大量的 kv 数据,它的定位类似于请求头,只适合存放少量作用域较大的全局 meta 数据: 一个 valueCtx 实例只能存一个 kv 对,因此 n 个 kv 对会嵌套 n 个 valueCtx,造成空间浪费;
基于 k 寻找 v 的过程是线性的,时间复杂度 O(N); 不支持基于 k 的去重,相同 k 可能重复存在,并基于起点的不同,返回不同的 v。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/1408.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何在在wordpress安装百度统计

前言 看过我的往期文章的都知道&#xff0c;我又建了一个网站&#xff0c;这次是来真的了。于是&#xff0c;最近在查阅资料时发现&#xff0c;有一款免费的软件可以帮我吗分析网站数据。&#xff08;虽然我的破烂网站压根没人访问&#xff0c;但是能装上的都得上&#xff0c;…

探索边缘计算:技术的新疆界

探索边缘计算&#xff1a;技术的新疆界 在当今迅速发展的数字化时代&#xff0c;云计算作为数据处理的主力军已广泛应用。但是&#xff0c;随着物联网&#xff08;IoT&#xff09;设备的急剧增加和数据生成速率的加快&#xff0c;云计算面临着种种挑战。边缘计算因此诞生&…

什么是Dubbo及其主要作用

在微服务架构日益盛行的今天&#xff0c;RPC&#xff08;远程过程调用&#xff09;技术成为了实现服务间通信的关键手段。而Dubbo&#xff0c;作为一款高性能、轻量级的开源Java RPC框架&#xff0c;受到了广大开发者的青睐。那么&#xff0c;Dubbo究竟是什么呢&#xff1f;它的…

STL-list的使用及其模拟实现

在C标准库中&#xff0c;list 是一个双向链表容器&#xff0c;用于存储一系列元素。与 vector 和 deque 等容器不同&#xff0c;list 使用带头双向循环链表的数据结构来组织元素&#xff0c;因此list插入删除的效率非常高。 list的使用 list的构造函数 list迭代器 list的成员函…

深度神经网络(DNN)

通过5个条件判定一件事情是否会发生&#xff0c;5个条件对这件事情是否发生的影响力不同&#xff0c;计算每个条件对这件事情发生的影响力多大&#xff0c;写一个深度神经网络&#xff08;DNN&#xff09;模型程序,最后打印5个条件分别的影响力。 示例 在深度神经网络&#xf…

动态规划相关

动态规划相关 力扣509 斐波那契数列 完全递归解法 / 设置备忘录减少递归次数解法 都是 自顶向下力扣 509 斐波那契数列 动态规划 自底向上 力扣509 斐波那契数列 完全递归解法 / 设置备忘录减少递归次数解法 都是 自顶向下 public int fib(int n) {/** if(n<2){* return n;…

Matlab新手快速上手2(粒子群算法)

本文根据一个较为简单的粒子群算法框架详细分析粒子群算法的实现过程&#xff0c;对matlab新手友好&#xff0c;源码在文末给出。 粒子群算法简介 粒子群算法&#xff08;Particle Swarm Optimization&#xff0c;PSO&#xff09;是一种群体智能优化算法&#xff0c;灵感来源于…

【测试总结】测试时如何定位一个bug?是前端还是后端?

作为一道面试题&#xff0c;它算高频了么&#xff1f;我面试别人问多挺多次&#xff0c;我也被面试官问过... 相对来说多少能看出一点测试经验&#xff0c;实际测试中的排查问题能力... 1、前后端bug有各自的一些特点&#xff1a; 前端bug特性&#xff1a;界面相关&#xff0c…

计算机网络(第7版谢希仁)笔记

计算机网络 第一章 概述第二章 物理层第三章、数据链路层第四章 网络层第五章 运输层第六章、应用层第七章 网络安全 第一章 概述 1、三大类网络&#xff1a;电信网络、有线电视网络、计算机网络。 电信网络&#xff1a;提供电话、电报及传真服务。 有线电视网络&#xff1a;向…

目标检测YOLO数据集的三种格式及转换

目标检测YOLO数据集的三种格式 在目标检测领域&#xff0c;YOLO&#xff08;You Only Look Once&#xff09;算法是一个流行的选择。为了训练和测试YOLO模型&#xff0c;需要将数据集格式化为YOLO可以识别的格式。以下是三种常见的YOLO数据集格式及其特点和转换方法。 1. YOL…

计算机系统结构(二) (万字长文建议收藏)

计算机系统结构 (二) 本文首发于个人博客网站&#xff1a;http://www.blog.lekshome.top/由于CSDN并不是本人主要的内容输出平台&#xff0c;所以大多数博客直接由md文档导入且缺少审查和维护&#xff0c;如果存在图片或其他格式错误可以前往上述网站进行查看CSDN留言不一定能够…

大话设计模式-里氏代换原则

里氏代换原则&#xff08;Liskov Substitution Principle&#xff0c;LSP&#xff09; 概念 里氏代换原则是面向对象设计的基本原则之一&#xff0c;由美国计算机科学家芭芭拉利斯科夫&#xff08;Barbara Liskov&#xff09;提出。这个原则定义了子类型之间的关系&#xff0…

【设计模式】7、decorate 装饰模式

文章目录 七、decorate 装饰模式7.1 饮料&#xff1a;类型配料7.1.1 drink_with_ingredient_test.go7.1.2 drink_with_ingredient.go7.1.3 drink.go 7.2 notifier7.2.1 notifier_decorator_test7.2.2 notifier_decorator7.2.3 notifier 7.3 idraw7.3.1 idraw_test7.3.2 idraw7.…

【人工智能基础】经典逻辑与归结原理

本章节的大部分内容与离散数学的命题、谓词两章重合。 假言推理的合式公式形式 R,R→P⇒PR,R∨P⇒P 链式推理 R→P,P→Q⇒R→QR∨P,P∨Q⇒R∨Q 互补文字&#xff1a;P和P 亲本子句&#xff1a;含有互补文字的子句 R∨P,P∨Q为亲本子句 注意&#xff1a; 必须化成析取范式…

命理八字之电子木鱼的代码实现

#uniapp# #电子木鱼# 不讲废话&#xff0c;上截图 目录结构如下图 功能描述&#xff1a; 点击一下&#xff0c;敲一下&#xff0c;伴随敲击声&#xff0c;可自动点击。自动点击需看视频广告&#xff0c;或者升级VIP会员。 疑点解答&#xff1a; 即animation动画的时候&…

Window中Jenkins部署asp/net core web主要配置

代码如下 D: cd D:\tempjenkins\src\ --git工作目录 dotnet restore -s "https://nuget.cdn.azure.cn/v3/index.json" --nuget dotnet build dotnet publish -c release -o %publishPath% --发布路径

装饰器学习

【一】什么是装饰器 装饰器指的就是为被装饰的对象添加新的功能 器 代表工具 增加后的调用方式不变 在不改变源代码和调用方式的基础上增加额外的新功能 【二】装饰器的用途 对上线后的程序进行新功能的增加和修改 给一个功能增加新的需求或者改变原来的程序运行逻辑 【…

Day08React——第八天

useEffect 概念&#xff1a;useEffect 是一个 React Hook 函数&#xff0c;用于在React组件中创建不是由事件引起而是由渲染本身引起的操作&#xff0c;比如发送AJAx请求&#xff0c;更改daom等等 需求&#xff1a;在组件渲染完毕后&#xff0c;立刻从服务器获取频道列表数据…

iOS RACScheduler 使用详解

RACScheduler 是 ReactiveCocoa 框架中的一个关键组件&#xff0c;用于在 iOS 开发中管理任务的并发执行。以下是如何详细使用 RACScheduler 的指南&#xff0c;以 Markdown 格式展示。 主要调度器 主线程调度器 用于在主线程上执行任务&#xff0c;通常用于 UI 更新操作。 …

Java:二叉树(1)

从现在开始&#xff0c;我们进入二叉树的学习&#xff0c;二叉树是数据结构的重点部分&#xff0c;在了解这个结构之前&#xff0c;我们先来了解一下什么是树型结构吧&#xff01; 一、树型结构 1、树型结构简介 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>…