深入理解 go context

打个广告:欢迎关注我的微信公众号,在这里您将获取更全面、更新颖的文章!

image

原文链接:深入理解 go context 欢迎点赞关注

context 介绍

context 是 Go 语言中用于处理并发操作的一个重要概念。context也被称作上下文,主要用于在 goroutine 之间传递截止日期、取消信号和其他请求范围的值。

什么是 Go Context

Context 是一个接口,它定义了以下方法:

type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key interface{}) interface{}
}
  1. Deadline(): 返回与 Context 关联的截止时间。如果没有设置截止时间,则返回 zero time.Time 和 false。
  2. Done(): 返回一个 channel,当 Context 被取消或者截止时间到达时,该 channel 会被关闭。
  3. Err(): 返回 Context 被取消的原因。如果 Context 还未被取消,则返回 nil。
  4. Value(key interface{}): 返回与 Context 关联的请求范围的值。

context 的作用

Go Context 主要用于以下几个方面:

  • 传递取消信号: 当一个长时间运行的操作被取消时,它可以及时停止并释放资源。
  • 设置截止时间: 当一个操作超过预期时间时,可以自动取消该操作,避免阻塞。
  • 携带请求范围的数据: 可以在 goroutine 之间传递一些请求相关的数据,如用户 ID、跟踪 ID 等。

Context底层实现

context 树状模型

go 提供了四种创建context的函数:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

可以看到在构建过程中都是基于 parent context 来构建子 context,子 context 还可以继续派生新的子 context 因此 context 是一个类似树状的模型:

context 的根节点

context 树的最原始的根节点通常是 context.Background()context.TODO(),他们底层都是基于 emptyCtx 实现的:

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

emptyCtx 的实现也特别简单,只是对 Context 接口的实现,逻辑比较简单这里就不赘述了:

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
}func (e *emptyCtx) String() string {switch e {case background:return "context.Background"case todo:return "context.TODO"}return "unknown empty Context"
}

这里有个问题:context.Background()context.TODO() 底层实现是一样的,那么为什么暴露两个函数给用户呢?

原因是它们拥有不同的语义

  • context.Background()

Background returns a non-nil, empty Context. It is never canceled, has no values, and has no deadline. It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests.

  • context.TODO()

TODO returns a non-nil, empty Context. Code should use context.TODO when it’s unclear which Context to use or it is not yet available (because the
surrounding function has not yet been extended to accept a Context parameter).

context.WithValue

WithValue 实际会返回 valueCtx类型:

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}
}type valueCtx struct {Contextkey, val any
}

valueCtx类型和链表的节点比较像,通过Context字段指向父节点,key 和 val 存储 valueCtx 的 key 、val 参数。

func main() {ctx := context.Background()ctx = context.WithValue(ctx, "a", 100)ctx = context.WithValue(ctx, "b", 200)ctx = context.WithValue(ctx, "c", 300)}

上面的代码执行后会形成类似下面的链表:

这里需要注意的:

  1. key 必须是可以比较的类型,推荐自定义一个 struct{} 类型
  2. WithValue 的结果是一个链表,查找复杂度是 O ( n ) O(n) O(n) 所以不要使用WithValue传递大量的key-val。

key-val 查找是通过 valueCtx.Value() 实现的,整个查找过程就是沿着链表的最后一个节点一个一个向上查找:

type valueCtx struct {Contextkey, val any
}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)}}
}

context.WithCancel

context.WithCancel 的底层实现是 withCancel 函数,withCancel 函数主要有两个功能:

  1. 调用 newCancelCtx 创建 cancelCtx 类型的 ctx 实例
  2. 调用 propagateCancel 将 ctx 实例挂载到父节点上
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := withCancel(parent)return c, func() { c.cancel(true, Canceled, nil) }
}func withCancel(parent Context) *cancelCtx {if parent == nil {panic("cannot create context from nil parent")}c := newCancelCtx(parent)  // 创建 cancelCtx 实例propagateCancel(parent, c) // 把当前节点return c
}func newCancelCtx(parent Context) *cancelCtx {return &cancelCtx{Context: parent}
}
cancelCtx 类
type cancelCtx struct {Contextmu       sync.Mutex            // protects following fieldsdone     atomic.Value          // of chan struct{}, created lazily, closed by first cancel callchildren map[canceler]struct{} // set to nil by the first cancel callerr      error                 // set to non-nil by the first cancel callcause    error                 // set to non-nil by the first cancel call
}

字段解释:

  • mu:就是一个互斥锁,保证并发安全的,所以context是并发安全的
  • done:用来做context的取消通知信号,之前的版本使用的是chan struct{}类型,现在用atomic.Value做锁优化
  • children:key是接口类型canceler,目的就是存储实现当前canceler接口的子节点,当根节点发生取消时,遍历子节点发送取消信号
  • error:当context取消时存储取消信息
propagateCancel 函数
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(), Cause(parent))returndefault:}if p, ok := parentCancelCtx(parent); ok {p.mu.Lock()if p.err != nil {// parent has already been canceledchild.cancel(false, p.err, p.cause)} else {if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()} else {goroutines.Add(1)go func() {select {case <-parent.Done():child.cancel(false, parent.Err(), Cause(parent))case <-child.Done():}}()}
}

propagateCancel 函数的逻辑也比较简单:

  1. 调用 parentCancelCtx 寻找可取消的父节点
  2. 如果找到了就把当前节点加入到父节点的 children 里面
  3. 如果没找到则需要起一个协程来监听父节点和当前节点的取消事件
  4. 挂载的目的是父节点取消是当前节点也能被取消

这里有个问题为什么没找到父节点,还要监听父节点的取消事件呢?原因是 parentCancelCtx 函数只能识别 *cancelCtx 类型的父节点,如果父节点是实现了 Context 类型的自定义类型或者是嵌套了*cancelCtx 就识别不出来,所以需要启动一个协程来监听自定类型或者嵌套类型的取消事件。

func parentCancelCtx(parent Context) (*cancelCtx, bool) {done := parent.Done()if done == closedchan || done == nil {return nil, false}p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)if !ok {return nil, false}pdone, _ := p.done.Load().(chan struct{})if pdone != done {return nil, false}return p, true
}

parentCancelCtx函数是寻找一个父节点,关键逻辑 cancelCtxKey + valuea()

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 == &cancelCtxKey 时:

  1. cancelCtx类型返回的自身
  2. timerCtx类型返回的 timerCtx.cancelCtx
cancel 函数

cancelCtx.cancel 是取消 ctx 的具体实现

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {if err == nil {panic("context: internal error: missing cancel error")}if cause == nil {cause = err}c.mu.Lock()if c.err != nil {c.mu.Unlock()return // already canceled}c.err = errc.cause = caused, _ := 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, cause)}c.children = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c)}
}

cancel 函数的实现也比较简单:

  1. 关闭 done channel
  2. 调用所有子节点的cancel函数,取消所有子节点
  3. 根据 removeFromParent 参数来决定是否要从删除父节点删除当前节点
    • 因为父节点取消而被动取消的情况 removeFromParent 为false
    • 当前节点主动取消的情况 removeFromParent 为 true

context.WithDeadline

context.WithDeadline 的逻辑也同样比较简单:

  1. 创建一个 timerCtx 类型的实例并返回
  2. 将 timerCtx 实例挂载到父节点上
  3. 启动一个定时器,定时调用 cancel 方法
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)}// 创建一个 timerCtx 类型的实例并返回c := &timerCtx{cancelCtx: newCancelCtx(parent),deadline:  d,}// 将 timerCtx 实例挂载到父节点上propagateCancel(parent, c)dur := time.Until(d)if dur <= 0 {c.cancel(true, DeadlineExceeded, nil) // deadline has already passedreturn c, func() { c.cancel(false, Canceled, nil) }}c.mu.Lock()defer c.mu.Unlock()if c.err == nil {// 启动一个定时器,定时调用 cancel 方法c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded, nil)})}return c, func() { c.cancel(true, Canceled, nil) }
}
timerCtx 类型
type timerCtx struct {*cancelCtxtimer *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, cause error) {c.cancelCtx.cancel(false, err, cause)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()
}
  1. timerCtx 内嵌了 cancelCtx 相当于 timerCtx 继承了 cancelCtx
  2. 其中 timer 字段是实现WithDeadline、WithTimeout 的关键,其原理就是启动一个定时器定时调用 cancel 方法;
  3. timer 字段是非并发安全的,所以对timer的操作需要先加锁;

context.WithTimeout

context.WithTimeout 是基于 context.WithDeadline 实现的,这里就不赘述了。

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

小结

上面介绍了 context 的实现原理,里面涉及了很多接口与结构体,下面通过类图串一下他们之间的关系:

Context 的使用

取消信号传递

package mainimport ("context""fmt""time"
)func worker(ctx context.Context) {for {select {case <-ctx.Done():fmt.Println("Worker received cancellation signal.")returndefault:// Simulate some worktime.Sleep(500 * time.Millisecond)fmt.Println("Working...")}}
}func main() {parentCtx, cancel := context.WithCancel(context.Background())go worker(parentCtx)// Simulate main program executiontime.Sleep(2 * time.Second)// Cancel the context to stop the workercancel()// Wait for the worker to finishtime.Sleep(1 * time.Second)
}

超时控制

package mainimport ("context""fmt""time"
)func operationWithTimeout(ctx context.Context) {select {case <-time.After(3 * time.Second): // Simulate some long operationfmt.Println("Operation completed.")case <-ctx.Done():fmt.Println("Operation canceled due to timeout.")}
}func main() {timeoutCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)defer cancel()operationWithTimeout(timeoutCtx)
}

截止时间

package mainimport ("context""fmt""time"
)func operationWithDeadline(ctx context.Context) {deadline, ok := ctx.Deadline()if ok {fmt.Printf("Operation must be completed before: %s\n", deadline)} else {fmt.Println("No specific deadline for the operation.")}// Simulate some operationtime.Sleep(2 * time.Second)select {case <-ctx.Done():fmt.Println("Operation canceled due to context deadline.")default:fmt.Println("Operation completed within the deadline.")}
}func main() {deadline := time.Now().Add(5 * time.Second)deadlineCtx, cancel := context.WithDeadline(context.Background(), deadline)defer cancel()operationWithDeadline(deadlineCtx)
}

请求范围的值传递

package mainimport ("context""fmt""sync"
)func processRequest(ctx context.Context, requestID int) {// Accessing request-scoped value from the contextuserID, ok := ctx.Value("userID").(int)if !ok {fmt.Println("Failed to get userID from context.")return}fmt.Printf("Processing request %d for user %d\n", requestID, userID)
}func main() {// Creating a parent context with a request-scoped valueparentCtx := context.WithValue(context.Background(), "userID", 123)var wg sync.WaitGroup// Simulating multiple requestsfor i := 1; i <= 3; i++ {wg.Add(1)go func(requestID int) {// Creating a child context for each requestchildCtx := context.WithValue(parentCtx, "requestID", requestID)processRequest(childCtx, requestID)wg.Done()}(i)}wg.Wait()
}

本文由mdnice多平台发布

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Mirror学习笔记(二) 传输协议

文章目录 一、KCP传输协议二、Telepathy 传输协议三、WebSockets传输协议四、多路复用传输&#xff1a;五、延迟模拟传输&#xff1a;六、Ignorance协议七、LiteNetLib协议八、FizzSteamworks协议九、FizzyFacepunch协议十、加密协议十一、Edgegap协议 一、KCP传输协议 KCP是M…

热力图大揭秘!Matplotlib教你如何画出让数据‘火辣辣‘的激情图!

1. 引言 嘿&#xff0c;小伙伴们&#xff01;今天咱们来点不一样的&#xff0c;走进Matplotlib的神奇世界&#xff0c;一起绘制那让人热血沸腾的热力图&#xff01;别误会&#xff0c;这可不是什么天气预报图&#xff0c;而是让数据“火辣辣”展现自我的秘密武器。想象一下&am…

Adobe XD安装破解

文章目录 下载链接安装教程打开软件 下载链接 通过百度网盘分享的文件&#xff1a;Adobe XD 57.rar 链接&#xff1a;https://pan.baidu.com/s/14v_8EeKSyAtZoXT2nofCtQ 提取码&#xff1a;6qxx 安装教程 以管理员身份运行 安装完成后点击关闭 稍微等待一会&#xff0c;不…

linux编写shell脚本字段部署redis6.x版本,docker使用shell脚本一键自动部署redis

1.先创建部署脚本 touch redis.sh2.粘贴部署脚本内容到redis.sh echo "在线安装redis开始...." docker pull redis:6.2.1 sudo mkdir -p /home/admin/redis/{data,conf} sudo touch /home/admin/redis/conf/redis.conf echo " #注释掉这部分&#xff0c;这是限…

Yolov5-v7.0使用CBAM注意力机制记录

Yolov5-v7.0使用CBAM注意力机制记录 一、CBAM实现代码 在model/common.py文件中加入如下代码&#xff1a; #############CBAM注意力机制############## class ChannelAttention(nn.Module):def __init__(self, in_planes, ratio16):super(ChannelAttention, self).__init__(…

力扣高频SQL 50题(基础版)第三十三题

文章目录 力扣高频SQL 50题&#xff08;基础版&#xff09;第三十三题610.判断三角形题目说明实现过程准备数据实现方式结果截图 力扣高频SQL 50题&#xff08;基础版&#xff09;第三十三题 610.判断三角形 题目说明 表: Triangle ----------------- | Column Name | Typ…

pytorch学习笔记2 创建tensor

1 从numpy导入 anp.array([2,3.3]) torch.from_numpy(a)bnp.ones([2,3]) torch.from_numpy(b)2从list导入 torch.tensor([2.,3.2]) torch.FloatTensor([2.,3.2]) torch.tensor([[2.,3.2],[1.,22.3]]) 3 未初始化的随机数据 Torch.empty()Torch.FloatTensor&#xff08;d1,d…

【MySQL】索引 【下】{聚簇索引VS非聚簇索引/创建主键索引/全文索引的创建/索引创建原则}

文章目录 1.聚簇索引 VS 非聚簇索引经典问题 2.索引操作创建主键索引唯一索引的创建普通索引的创建全文索引的创建查询索引删除索引索引创建原则 1.聚簇索引 VS 非聚簇索引 之前介绍的将所有的数据都放在叶子节点的这种存储引擎对应的就是 InnoDB 默认存储表数据的存储结构。 …

MySQL从jsonarray获取某个字段的所有数据

表结构。表里的order_goods_info_vo_list是jsonarray字段 CREATE TABLE pdd_charge_back_bantuo (id int(11) NOT NULL AUTO_INCREMENT,shopname varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 店铺名,charge_back_sn varchar(64) COLLATE utf8mb4_bin DEFAULT NULL …

2000-2023年上市公司财务困境数据Oscore模型(含原始数据+计算结果)

2000-2023年上市公司财务困境数据Oscore模型&#xff08;含原始数据计算结果&#xff09; 1、2000-2023年 2、指标&#xff1a;证券代码、证券简称、统计截止日期、是否发生ST或*ST或PT、是否发生暂停上市、行业代码、行业名称、上市日期、总资产&#xff08;元&#xff09;、…

Docker Desktop安装(通俗易懂)

1、官网 https://www.docker.com/products/docker-desktop/ 2、阿里云镜像 docker-toolbox-windows-docker-for-windows安装包下载_开源镜像站-阿里云 1. 双击安装文件勾选选项 意思就是&#xff1a; Use WSL 2 instead of Hyper-V (recommended) : 启用虚拟化&#xff0c;…

IDEA对线上项目远程debug

1、在启动脚本上添加以下配置内容 -agentlib:jdwptransportdt_socket,servery,suspendn,address*:5005 nohup java -agentlib:jdwptransportdt_socket,servery,suspendn,address5005 -jar test.jar > misc.out & 2、在IDEA中进行配置 &#xff08;1&#xff09;选择远程…

红外激光模组如何升级为现代科技的璀璨明珠

在日新月异的科技领域中&#xff0c;红外激光模组以其独特的应用价值和卓越的性能&#xff0c;成为了众多行业的宠儿。从通信、测距、监控到医疗&#xff0c;红外激光模组以其广泛的应用场景&#xff0c;不断推动着现代科技的进步与发展。接下来我们就跟着鑫优威一起来了解一下…

详解Qt 之QByteArray

文章目录 详解Qt之QByteArray前言QByteArray概念作用为什么需要 QByteArrayQByteArray 的主要函数和成员函数列表 示例代码示例 1&#xff1a;字节数组的基本操作示例 2&#xff1a;数据编码和解码示例 3&#xff1a;字节数组的字符串操作 更多用法... 总结 详解Qt之QByteArray…

vue3+fetch请求+接收到流式的markdown数据+一边gpt打字机式输出内容,一边解析markdown语法+highlight.js实现代码高亮

这个问题终于解决了&#xff01;好开心。 先看最终效果&#xff1a; video_20240724_141543_edit 项目背景&#xff1a;vue3 场景&#xff1a;像gpt一样可以对话&#xff0c;当用户发送问题之后&#xff0c;ai回复&#xff0c;ai是一部分一部分回复&#xff0c;像打印机式输出…

微服务-服务拆分-服务远程调用

查询订单demo 通过Bean的方式将RestTemplate注册为Spring的一个对象&#xff0c;即注入Spring容器&#xff08;要写在配置类中&#xff0c;启动类本身就是配置类&#xff09;。然后在任何地方都可以注入该对象使用。 Eureka注册中心 Eureka服务搭建 Eureka客户端注册 配置服务…

Java与模式及其应用场景知识点分享(电子版)

前言 Java 编程语言自1995年问世以来&#xff0c;其成功好像任何编程语言都无法媲美。生逢其时(互联网的兴起)固然是一方面的原因&#xff0c;而Java吸收总结了前人的经验教训&#xff0c;反映了最新技术(the state ofthe art)&#xff0c;对其受到欢迎和采用&#xff0c;恐怕…

如何在基于滤波框架的绝对定位系统中融合相对观测

文章目录 1 LIO、VIO propagation来代替IMU propagation2 TRO paper: Stochastic Cloning Kalman filter【有待填坑】 以无人驾驶定位系统为例&#xff0c;融合gnss&#xff0c;imu&#xff0c;轮速&#xff0c;camera LaneMatch(frame to map)&#xff0c;lidar scan match(fr…

大数据-55 Kafka sh脚本使用 与 JavaAPI使用 topics.sh producer.sh consumer.sh kafka-clients

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; 目前已经更新到了&#xff1a; Hadoop&#xff08;已更完&#xff09;HDFS&#xff08;已更完&#xff09;MapReduce&#xff08;已更完&am…

如何有效管理众多账号密码:选择适合你的密码管理工具

在如今的数字化时代&#xff0c;我们的生活几乎离不开各种互联网应用和服务。从社交媒体到在线银行&#xff0c;从购物网站到工作平台&#xff0c;每个应用都要求我们注册账号并设置密码。 随着账号数量的不断增加&#xff0c;管理这些密码成为了一个令人头疼的问题。幸运的是…