通关GO语言10 Context:你必须掌握的多线程并发控制神器

news/2024/5/21 21:17:20/文章来源:https://blog.csdn.net/fegus/article/details/126915501

在上一节课中我留了一个作业,也就是让你自己练习使用 sync.Map,相信你已经做出来了。现在我为你讲解 sync.Map 的方法。

  1. Store:存储一对 key-value 值。

  2. Load:根据 key 获取对应的 value 值,并且可以判断 key 是否存在。

  3. LoadOrStore:如果 key 对应的 value 存在,则返回该 value;如果不存在,存储相应的 value。

  4. Delete:删除一个 key-value 键值对。

  5. Range:循环迭代 sync.Map,效果与 for range 一样。

相信有了这些方法的介绍,你对 sync.Map 会有更深入的理解。下面开始今天的课程:如何通过 Context 更好地控制并发。

协程如何退出

一个协程启动后,大部分情况需要等待里面的代码执行完毕,然后协程会自行退出。但是如果有一种情景,需要让协程提前退出怎么办呢?在下面的代码中,我做了一个监控狗用来监控程序:

ch10/main.go

func main() {var wg sync.WaitGroupwg.Add(1)go func() {defer wg.Done()watchDog("【监控狗1】")}()wg.Wait()
}
func watchDog(name string){//开启for select循环,一直后台监控for{select {default:fmt.Println(name,"正在监控……")}time.Sleep(1*time.Second)}
}

我通过 watchDog 函数实现了一个监控狗,它会一直在后台运行,每隔一秒就会打印"监控狗正在监控……"的文字。

如果需要让监控狗停止监控、退出程序,一个办法是定义一个全局变量,其他地方可以通过修改这个变量发出停止监控狗的通知。然后在协程中先检查这个变量,如果发现被通知关闭就停止监控,退出当前协程。

但是这种方法需要通过加锁来保证多协程下并发的安全,基于这个思路,有个升级版的方案:用 select+channel 做检测,如下面的代码所示:

ch10/main.go

func main() {var wg sync.WaitGroupwg.Add(1)stopCh := make(chan bool) //用来停止监控狗go func() {defer wg.Done()watchDog(stopCh,"【监控狗1】")}()time.Sleep(5 * time.Second) //先让监控狗监控5秒stopCh <- true //发停止指令wg.Wait()
}
func watchDog(stopCh chan bool,name string){//开启for select循环,一直后台监控for{select {case <-stopCh:fmt.Println(name,"停止指令已收到,马上停止")returndefault:fmt.Println(name,"正在监控……")}time.Sleep(1*time.Second)}
}

这个示例是使用 select+channel 的方式改造的 watchDog 函数,实现了通过 channel 发送指令让监控狗停止,进而达到协程退出的目的。以上示例主要有两处修改,具体如下:

  1. 为 watchDog 函数增加 stopCh 参数,用于接收停止指令;

  2. 在 main 函数中,声明用于停止的 stopCh,传递给 watchDog 函数,然后通过 stopCh<-true 发送停止指令让协程退出。

初识 Context

以上示例是 select+channel 比较经典的使用场景,这里也顺便复习了 select 的知识。

通过 select+channel 让协程退出的方式比较优雅,但是如果我们希望做到同时取消很多个协程呢?如果是定时取消协程又该怎么办?这时候 select+channel 的局限性就凸现出来了,即使定义了多个 channel 解决问题,代码逻辑也会非常复杂、难以维护。

要解决这种复杂的协程问题,必须有一种可以跟踪协程的方案,只有跟踪到每个协程,才能更好地控制它们,这种方案就是 Go 语言标准库为我们提供的 Context,也是这节课的主角。

现在我通过 Context 重写上面的示例,实现让监控狗停止的功能,如下所示:

ch10/main.go

func main() {var wg sync.WaitGroupwg.Add(1)ctx,stop:=context.WithCancel(context.Background())go func() {defer wg.Done()watchDog(ctx,"【监控狗1】")}()time.Sleep(5 * time.Second) //先让监控狗监控5秒stop() //发停止指令wg.Wait()
}
func watchDog(ctx context.Context,name string) {//开启for select循环,一直后台监控for {select {case <-ctx.Done():fmt.Println(name,"停止指令已收到,马上停止")returndefault:fmt.Println(name,"正在监控……")}time.Sleep(1 * time.Second)}
}

相比 select+channel 的方案,Context 方案主要有 4 个改动点。

  1. watchDog 的 stopCh 参数换成了 ctx,类型为 context.Context。

  2. 原来的 case <-stopCh 改为 case <-ctx.Done(),用于判断是否停止。

  3. 使用 context.WithCancel(context.Background()) 函数生成一个可以取消的 Context,用于发送停止指令。这里的 context.Background() 用于生成一个空 Context,一般作为整个 Context 树的根节点。

  4. 原来的 stopCh <- true 停止指令,改为 context.WithCancel 函数返回的取消函数 stop()。

可以看到,这和修改前的整体代码结构一样,只不过从 channel 换成了 Context。以上示例只是 Context 的一种使用场景,它的能力不止于此,现在我来介绍什么是 Context。

什么是 Context

一个任务会有很多个协程协作完成,一次 HTTP 请求也会触发很多个协程的启动,而这些协程有可能会启动更多的子协程,并且无法预知有多少层协程、每一层有多少个协程。

如果因为某些原因导致任务终止了,HTTP 请求取消了,那么它们启动的协程怎么办?该如何取消呢?因为取消这些协程可以节约内存,提升性能,同时避免不可预料的 Bug。

Context 就是用来简化解决这些问题的,并且是并发安全的。Context 是一个接口,它具备手动、定时、超时发出取消信号、传值等功能,主要用于控制多个协程之间的协作,尤其是取消操作。一旦取消指令下达,那么被 Context 跟踪的这些协程都会收到取消信号,就可以做清理和退出操作。

Context 接口只有四个方法,下面进行详细介绍,在开发中你会经常使用它们,你可以结合下面的代码来看。

type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key interface{}) interface{}
}
  1. Deadline 方法可以获取设置的截止时间,第一个返回值 deadline 是截止时间,到了这个时间点,Context 会自动发起取消请求,第二个返回值 ok 代表是否设置了截止时间。

  2. Done 方法返回一个只读的 channel,类型为 struct{}。在协程中,如果该方法返回的 chan 可以读取,则意味着 Context 已经发起了取消信号。通过 Done 方法收到这个信号后,就可以做清理操作,然后退出协程,释放资源。

  3. Err 方法返回取消的错误原因,即因为什么原因 Context 被取消。

  4. Value 方法获取该 Context 上绑定的值,是一个键值对,所以要通过一个 key 才可以获取对应的值。

Context 接口的四个方法中最常用的就是 Done 方法,它返回一个只读的 channel,用于接收取消信号。当 Context 取消的时候,会关闭这个只读 channel,也就等于发出了取消信号。

Context 树

我们不需要自己实现 Context 接口,Go 语言提供了函数可以帮助我们生成不同的 Context,通过这些函数可以生成一颗 Context 树,这样 Context 才可以关联起来,父 Context 发出取消信号的时候,子 Context 也会发出,这样就可以控制不同层级的协程退出。

从使用功能上分,有四种实现好的 Context。

  1. 空 Context:不可取消,没有截止时间,主要用于 Context 树的根节点。

  2. 可取消的 Context:用于发出取消信号,当取消的时候,它的子 Context 也会取消。

  3. 可定时取消的 Context:多了一个定时的功能。

  4. 值 Context:用于存储一个 key-value 键值对。

从下图 Context 的衍生树可以看到,最顶部的是空 Context,它作为整棵 Context 树的根节点,在 Go 语言中,可以通过 context.Background() 获取一个根节点 Context。

Drawing 1.png

(四种 Context 的衍生树)

有了根节点 Context 后,这颗 Context 树要怎么生成呢?需要使用 Go 语言提供的四个函数。

  1. WithCancel(parent Context):生成一个可取消的 Context。

  2. WithDeadline(parent Context, d time.Time):生成一个可定时取消的 Context,参数 d 为定时取消的具体时间。

  3. WithTimeout(parent Context, timeout time.Duration):生成一个可超时取消的 Context,参数 timeout 用于设置多久后取消

  4. WithValue(parent Context, key, val interface{}):生成一个可携带 key-value 键值对的 Context。

以上四个生成 Context 的函数中,前三个都属于可取消的 Context,它们是一类函数,最后一个是值 Context,用于存储一个 key-value 键值对。

使用 Context 取消多个协程

取消多个协程也比较简单,把 Context 作为参数传递给协程即可,还是以监控狗为例,如下所示:

ch10/main.go

wg.Add(3)
go func() {defer wg.Done()watchDog(ctx,"【监控狗2】")
}()
go func() {defer wg.Done()watchDog(ctx,"【监控狗3】")
}()

示例中增加了两个监控狗,也就是增加了两个协程,这样一个 Context 就同时控制了三个协程,一旦 Context 发出取消信号,这三个协程都会取消退出。

以上示例中的 Context 没有子 Context,如果一个 Context 有子 Context,在该 Context 取消时会发生什么呢?下面通过一幅图说明:

Drawing 3.png

(Context 取消)

可以看到,当节点 Ctx2 取消时,它的子节点 Ctx4、Ctx5 都会被取消,如果还有子节点的子节点,也会被取消。也就是说根节点为 Ctx2 的所有节点都会被取消,其他节点如 Ctx1、Ctx3 和 Ctx6 则不会。

Context 传值

Context 不仅可以取消,还可以传值,通过这个能力,可以把 Context 存储的值供其他协程使用。我通过下面的代码来说明:

ch10/main.go

func main() {wg.Add(4) //记得这里要改为4,原来是3,因为要多启动一个协程

//省略其他无关代码
valCtx:=context.WithValue(ctx,“userId”,2)
go func() {
defer wg.Done()
getUser(valCtx)
}()
//省略其他无关代码
}
func getUser(ctx context.Context){
for {
select {
case <-ctx.Done():
fmt.Println(“【获取用户】”,“协程退出”)
return
default:
userId:=ctx.Value(“userId”)
fmt.Println(“【获取用户】”,“用户ID为:”,userId)
time.Sleep(1 * time.Second)
}
}
}

这个示例是和上面的示例放在一起运行的,所以我省略了上面实例的重复代码。其中,通过 context.WithValue 函数存储一个 userId 为 2 的键值对,就可以在 getUser 函数中通过 ctx.Value("userId") 方法把对应的值取出来,达到传值的目的。

Context 使用原则

Context 是一种非常好的工具,使用它可以很方便地控制取消多个协程。在 Go 语言标准库中也使用了它们,比如 net/http 中使用 Context 取消网络的请求。

要更好地使用 Context,有一些使用原则需要尽可能地遵守。

  1. Context 不要放在结构体中,要以参数的方式传递。

  2. Context 作为函数的参数时,要放在第一位,也就是第一个参数。

  3. 要使用 context.Background 函数生成根节点的 Context,也就是最顶层的 Context。

  4. Context 传值要传递必须的值,而且要尽可能地少,不要什么都传。

  5. Context 多协程安全,可以在多个协程中放心使用。

以上原则是规范类的,Go 语言的编译器并不会做这些检查,要靠自己遵守。

总结

Context 通过 With 系列函数生成 Context 树,把相关的 Context 关联起来,这样就可以统一进行控制。一声令下,关联的 Context 都会发出取消信号,使用这些 Context 的协程就可以收到取消信号,然后清理退出。你在定义函数的时候,如果想让外部给你的函数发取消信号,就可以为这个函数增加一个 Context 参数,让外部的调用者可以通过 Context 进行控制,比如下载一个文件超时退出的需求。

这节课的最后留一个思考题给你:假如一个用户请求访问我们的网站,如何通过 Context 实现日志跟踪?先自己想想,下节课我会揭晓思路。

下节课将学习“并发模式:Go 语言中即学即用的高效并发模式”,记得来听课!


精选评论

**3439:

请教老师两个问题1.正常情况下,主协程退出后,子协程无法立即自动退出是吗?但是我理解应该是子协程执行完了它自己就会退出,只是不受主协程的影响,老师说不可预知的事情是否是指子协程可能永不结束,那样长期占用内存造成内存泄露,还有其他危险情况吗?2.使用context,是为了让子协程们受主协程的控制,如果主协程执行完毕,是否必须手动stop才能让各子协程退出?如果主协程执行完退出,是否会自动触发stop,各子协程也相继关闭呢?

    讲师回复:

    主线程退出,整个程序就死了。这节的本意是在主线程不退出,也就是程序正常的情况下,使用Context更好的控制子线程。

*斌:

老师,能不能说一下,context.TODO()和context.Background()在项目中应该怎么使用?

    讲师回复:

    优先使用 Background,不知道使用什么的话,再选择 TODO

*翔:

老师,我用context保存认证成功后当前请求登陆的用户信息,这个使用正确吗

*凯:

对于用户登陆的逻辑来说,是不是我可以在登陆的时候使用带过期时间的context,起一个携程专门监控用户登陆有效期,如果过期触发登出逻辑?另外,老师说的监控用户访问网站行为,是否可以在context 中以键值对形式存储用户访问当前页地址,key为用户id

    讲师回复:

    不要过渡使用Context,登录有效期的Token可以存放在其他地方,比如Redis。
网站访问的行为也是,可以使用Log打点,存在ELK等一些专门的日志处理软件中

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

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

相关文章

GO面试题集锦

GO面试题集锦 目录GO面试题集锦slice 扩容机制slice 为什么不是线程安全的map 底层原理map 扩容机制map 遍历为什么无序map 为什么不是线程安全的Map 如何查找Map 冲突解决方式Map 负载因子为什么是 6.5Map 和 Sync.Map 哪个性能好Channel 底层实现原理Channel 有什么特点Chann…

docker实战教程(七):镜像的分层概念

联合文件系统(UnionFS) 联合文件系统是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。联合文件系统是docker镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像…

Zookeeper简介

文章目录Zookeeper简介zookeeper能做什么zookeeper的数据模型zookeeper工作机制zookeeper集群的选举机制1、第一次启动选举机制2、非第一次启动选举机制搭建zookeeper的集群Zookeeper简介 zookeeper能做什么 master节点选举&#xff1a;主节点挂了以后&#xff0c;从节点就会…

基于 ANFIS 的非线性回归(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f468;‍&#x1f393;博主课外兴趣&#xff1a;中西方哲学&#xff0c;送予读者&#xff1a; &#x1f468;‍&a…

字符串函数以及内存函数的模拟实现(超详细,全面理解字符串函数!!!)

目录 一、strlen 1.参数指向的字符串必须要以 \0 结束。 2.注意strlen函数的返回值为size_t&#xff0c;是无符号的 3.模拟实现strlen 二、strcpy 1.源字符串中的 \0 拷贝到目标空间 2.源字符串必须以 \0 结束 3.目标空间必须足够大&#xff0c;以确保能存放源字符串 4…

@Conditional

条件装配:满足Conditional指定的条件,则进行组件注入 @Configuration//告诉springboot这是一个配置类 public class MyConfig {@Bean("tom")public Stu stu01(){return new Stu("汤姆");}@ConditionalOnBean(name="tom")//当容器中有tom组件时…

windows工具:推荐一款可以截长图(滚动截图)的工具FSCapture

windows工具&#xff1a;推荐一款可以截长图&#xff08;滚动截图&#xff09;的工具前言一、FSCapture是什么&#xff1f;二、使用方法1.下载地址和安装2.使用方法前言 有的时候你画的框架图太大&#xff0c;已经超过了一屏&#xff0c;想要导出图片&#xff0c;用普通窗口截…

汇编常用寄存器以及寻址方式

寄存器概览 常用寄存器 AX accumulator 累加寄存器 BX base 基址寄存器 CX count 计数寄存器 DX data 数据寄存器 SP stack pointer 堆栈寄存器 BP base pointer 基址指针寄存器 SI source index 源变址寄存器 DI destination index 目的变址寄存器 IP instruction pointer 指…

ch4 报错修正 Sophus使用

ch4 报错& 修正 &#xff08;1&#xff09; # 添加Eigen头文件 include_directories( "/usr/include/eigen3" )&#xff08;2&#xff09; #include "sophus/so3.hpp" #include "sophus/se3.hpp"&#xff08;3&#xff09; 大量报错但都…

定制qga(作业截图)

文章目录一、qga介绍二、证明qga命令可以正常使用三、创建qga安装包四、总步骤一、qga介绍 qemu guest agent简称qga&#xff0c; 是运行在虚拟机内部的一个守护程序&#xff08;qemu-guest-agent.service&#xff09;&#xff0c; 他可以管理应用程序&#xff0c;执行宿主机发…

声呐直线阵正交混频实验(HEU信息与信号处理创新实践项目一)

写在前面 这个实验原要求是要实现 969696 通道的正交混频变换&#xff08;后来老师说只要不是单通道都行&#xff09;&#xff0c;因此必须使用 FIRFIRFIR IP核&#xff08;手搓FIR一两个通道还行&#xff0c;96通道就太费劲了&#xff09;&#xff0c;所以实验成功的关键就是…

BNU002期-学术沙龙-写好综述

文章目录综述的介绍什么是综述为什么要读综述为什么要写综述怎样写综述综述案例中的问题对于综述写作问题的分类如何避免综述写作问题讨论综述问题框架环节并完善做个升华&#xff1a;谈谈科研和读综述的乐趣本文引用资料的链接补充综述的介绍 本文围绕 什么是综述 我创设这…

微服务基础---认识微服务

1.1认识微服务 1.1.1微服务架构演变 单体架构 将业务的所有功能都集中在一个项目中进行开发&#xff0c;打成一个包部署. 优点&#xff1a;架构简单、部署成本低缺点&#xff1a;耦合度高 分布式架构 根据业务功能对系统进行拆分&#xff0c;每个业务模块作为独立项目开发&am…

软件流程和管理(八):Ethics

目录 1. Ethics 1.1 道德&#xff08;Ethics&#xff09;是什么&#xff1f; 1.2 关于计算机伦理的错误假设 1.3 为什么你要关心建立道德技能和知识 1.4 信息技术的道德责任 1.5 澳大利亚计算机协会的道德准则 1.6 组织中的道德是很重要的 1.7 道德&#xff1a;实用指…

zephyr线程生命周期

ephyr中线程是使用CPU的最小单位&#xff0c;线程从创建后由zephyr内核进行调度&#xff0c;根据运行和等待资源的状况在几个状态中切换&#xff0c;直到线程终止退出生命周期。 线程状态 线程在其生命周期中有下面6种状态&#xff1a; New 创建&#xff1a;线程被创建起来但…

实验2:Open vSwitch虚拟交换机实践

(一)基本要求1.ovs-vsctl基础操作实践:创建OVS交换机,以ovs-xxxxxxxxx命名,其中xxxxxxxxx为本人学号。在创建的交换机上增加端口p0和p1,设置p0的端口号为100,p1的端口号为101,类型均为internal;为了避免网络接口上的地址和本机已有网络地址冲突,需要创建虚拟网络空间…

Redis实现消息队列(双端队列的模式,发布订阅模式)

文章目录 1 采用双端队列的模式1.1 入队出队操作1.2 生产者编写1.3 消费者编写1.4 测试2 采用发布订阅模式2.1 编写生产者2.2 编写消费者2.3 测试​ 本部分,我们使用 redis实现消息队列的功能,采用 redis实现消息队列主要有两种方式:采用 redis自带双端队列实现;采用 r…

【牛客刷题-算法】NC7 买卖股票的最好时机(一)

个人主页&#xff1a;清风莫追 系列专栏&#xff1a;牛客刷题——数据结构与算法 推荐一款面试、刷题神器牛客网&#xff1a;&#x1f449;点击开始刷题学习&#x1f448; 文章目录1.题目描述2.算法设计思路3.代码实现4.运行结果结束语&#xff1a;1.题目描述 描述 假设你有一…

Android移动应用开发之ImageView、ProgressBar和Notification的一些简单使用

文章目录主要文件目录MainActivity:NotificationActivitya.pngic_baseline_account_box_24.xmlactivity_main运行主要文件目录 MainActivity: 这里主要用于按钮响应处理和通知处理 package zufe.scq.hunter;import androidx.appcompat.app.AppCompatActivity; import android…

Letcode动态规划专题-困难

10. 正则表达式匹配 42. 接雨水 1.传统方式-按照行的方式计算 整个思路就是&#xff0c;求第 i 层的水&#xff0c;遍历每个位置&#xff0c;如果当前的高度小于 i&#xff0c;并且两边有高度大于等于 i 的&#xff0c;说明这个地方一定有水&#xff0c;水就可以加 11。 如…