Go语言并发之context标准库

news2024/11/27 10:23:20

1、Go语言并发之context标准库

Go中的 goroutine 之间没有父与子的关系,也就没有所谓子进程退出后的通知机制,多个 goroutine 都是平行地

被调度,多个 goroutine 如何协作工作涉及通信、同步、通知和退出四个方面。

  • 通信:chan 通道当然是 goroutine 之间通信的基础,注意这里的通信主要是指程序的数据通道。

  • 同步:不带缓冲的 chan 提供了一个天然的同步等待机制;当然 sync.WaitGroup 也为多个 goroutine 协同工

    作提供一种同步等待机制。

  • 通知:这个通知和上面通信的数据不一样,通知通常不是业务数据,而是管理、控制流数据。要处理这个也好

    办,在输入端绑定两个 chan,一个用于业务流数据,另一个用于异常通知数据,然后通过 select 收敛进行处

    理。这个方案可以解决简单的问题,但不是一个通用的解决方案。

  • 退出:goroutine 之间没有父子关系,如何通知 goroutine 退出?可以通过增加一个单独的通道,借助通道和

    select 的广播机制实现退出。

Go语言在语法上处理某个 goroutine 退出通知机制很简单。但是遇到复杂的并发结构处理起来就显得力不从心。

实际编程中 goroutine 会拉起新的 goroutine,新的 goroutine 又会拉起另一个新的 goroutine,最终形成一个树

状的结构,由于 goroutine 里并没有父子的概念,这个树状的结构只是在程序员头脑中抽象出来的,程序的执行

模型并没有维护这么一个树状结构。怎么通知这个树状上的所有 goroutine 退出?仅依靠语法层面的支持显然比

较难处理。为此 Go1.7 提供了一个标准库 context 来解决这个问题。它提供两种功能:退出通知和元数据传递。

context 库的设计目的就是跟踪 goroutine 调用,在其内部维护一个调用树,并在这些调用树中传递通知和元数

据。

1.1 Context的设计目的

context 库的设计目的就是跟踪 goroutine 调用树,并在这些 gouroutine 调用树中传递通知和元数据。两个目

的:

(1)、退出通知机制:通知可以传递给整个goroutine 调用树上的每一个goroutine。

(2)、传递数据:数据可以传递给整个goroutine 调用树上的每一个goroutine。

1.2 基本数据结构

在介绍 context 包之前,先理解context包的整体工作机制:第一个创建Context的goroutine被称为root节点。

root节点负责创建一个实现Context接口的具体对象,并将该对象作为参数传递到其新拉起的goroutine,下游的

goroutine可以继续封装该对象,再传递到更下游的goroutine。

Context 对象在传递的过程中最终形成一个树状的数据结构,这样通过位干root节点(树的根节点)的Context 对象

就能遍历整个Context 对象树,通知和消息就可以通过root节点传递出去,实现了上游 goroutine 对下游

goroutine的消息传递。

1.2.1 Context接口

Context 是一个基本接口,所有的 Context 对象都要实现该接口,context 的使用者在调用接口中都使用 Context

作为参数类型。

type Context interface{
	
    // 如果Context实现了超时控制,则该方法返回ok true,deadline为超时时间,否则ok为false
	Deadline() (deadline time.Time,ok bool)
	
    // 后端被调的goroutine应该监听该方法返回的chan,以便及时释放资源
    Done() <-chan struct{}
	
    // Done返回的chan收到通知的时候,才可以访问Err()获知因为什么原因被取消
	Err()error
	
    // 可以访问上游 goroutine 传递给下游 goroutine 的值
	Value(key interface{}) interface{}
}

1.2.2 canceler接口

canceler 接口是一个扩展接口,规定了取消通知的 Context 具体类型需要实现的接口。

context 包中的具体类型 cancelctx 和 timerCtx 都实现了该接口。

1.2.3 empty Context结构

emptyCtx 实现了 Context 接口,但不具备任何功能,因为其所有的方法都是空实现。其存在的目的是作为

Context 对象树的根(root 节点)。因为context包的使用思路就是不停地调用context 包提供的包装函数来创建具有

特殊功能的Context 实例,每一个Context 实例的创建都以上一个Context对象为参数,最终形成一个树状的结

构。

1.2.4 cancelCtx

cancelCtx 是一个实现了Context接口的具体类型,同时实现了conceler接口。conceler具有退出通知方法。注意

退出通知机制不但能通知己,也能逐层通知其children 节点。

1.2.5 timerCtx

timerCtx 是一个实现了Context接口的具体类型,内部封装了cancelCtx类型实例,同时有一个 deadline 变量,

用来实现定时退出通知。

1.2.6 valueCtx

valueCtx 是一个实现了Context接口的具体类型,内部封装了Context接口类型,同时封装了一个k/v的存储变

量,valueCtx 可用来传递通知信息。

1.3 API函数

下面这两个函数是构造 Context 取消树的根节点对象,根节点对象用作后续 With 包装函数的实参。

func Background() Context
func TODO() Context

With 包装函数用来构建不同功能的 Context 具体对象。

(1)、创建一个带有退出通知的 Context 具体对象,内部创建一个 cancelCtx 的类型实例。

func WithCancel(parent Context)(Context,CancelFunc)

(2)、创建一个带有超时通知的 Context 具体对象,内部创建一个 timerCtx 的类型实例。

func WithDeadline(parent Context,deadline time.Time)(Context,CancelFunc)

(3)、创建一个带有超时通知的 Context 具体对象,内部创建一个 timerCtx 的类型实例。

func WithTimeout(parent Context,timeout time.Duration)(Context,CancelFunc)

(4)、创建一个能够传递数据的 Context 具体对象,内部创建一个 valueCtx 的类型实例。

func WithValue(parent Context,key,val interface{})Context

这些函数都有一个共同的特点—— parent 参数,其实这就是实现 Context 通知树的必备条件。在 goroutine 的

调用链中,Context 的实例被逐层地包装并传递,每层又可以对传讲来的 Context 实例再封装自己所需的功能,

整个调用树需要一个数据结构来维护,这个维护逻辑在这些包装函数内部实现。

1.4 辅助函数

前面描述的 With 开头的构造函数是给外部程序使用的 API 接口函数。Context 具体对象的链条关系是在 With 函

数的内部维护的。

func propagateCancel(parent Context,child canceler)

1.5 context的用法

package main

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

// 定义一个新的类型包含Context字段
type otherContext struct {
	context.Context
}

func main() {
	// 创建一个带有退出通知的Context具体对象:*cancelCtx
	ctxA, cancel := context.WithCancel(context.Background())
	go work(ctxA, "work1")
	// 超时3秒计算
	tm := time.Now().Add(3 * time.Second)
	// 创建一个带有超时通知的Context具体对象:*timerCtx
	ctxB, _ := context.WithDeadline(ctxA, tm)
	go work(ctxB, "work2")
	oc := otherContext{ctxB}
	// 创建一个能够传递数据的Context具体对象:*cancelCtx
	ctxC := context.WithValue(oc, "key", "god andes,pass from main ")
	go workWithValue(ctxC, "work3")
	time.Sleep(10 * time.Second)
	cancel()
	// 这5秒没有什么作用
	time.Sleep(5 * time.Second)
	fmt.Println("main stop")
}

// 工作
func work(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("%s get msg to cancel\n", name)
			return
		default:
			fmt.Printf("%s is running \n", name)
			time.Sleep(1 * time.Second)
		}
	}
}

// 根据context传递值
func workWithValue(ctx context.Context, name string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("%s get msg to cancel\n", name)
			return
		default:
			value := ctx.Value("key").(string)
			fmt.Printf("%s is running value=%s \n", name, value)
			time.Sleep(1 * time.Second)
		}
	}
}
# 程序输出
work3 is running value=god andes,pass from main
work1 is running
work2 is running
work1 is running
work3 is running value=god andes,pass from main
work2 is running
work3 is running value=god andes,pass from main
work2 is running
work1 is running
work1 is running
work3 get msg to cancel
work2 get msg to cancel
work1 is running
work1 is running
work1 is running
work1 is running
work1 is running
work1 is running
work1 get msg to cancel
main stop

上面的输出中:

work1 is running会输出10次,因为会休眠10秒。

work2 is running会输出3次,因为超时时间为3秒。

work3 is running也会输出3次,因为它的context取决于work2的context。

在使用 Context 的过程中,程序在底层实际上维护了两条关系链,理解这个关系链对理解 context 包非常有好

处,两条引用关系链如下。

(1)、children key 构成从根到叶子 Context 实例的引用关系,这个关系在调用 With 函数时进行维护(调用上文

介绍的 propagateCancel(parent Context,child canceler) 函数维护),程序有一层这样的树状结构(本示例

是一个链表结构):

ctxa.children--->ctxb
ctxb.children--->ctxc

这个树提供一种从根节点开始遍历树的方法,context 包的取消广播通知的核心就是基于这一点实现的。取消通

知沿着这条链从根节点向下层节点逐层广播。当然也可以在任意一个子树上调用取消通知,一样会扩散到整棵树。

示例程序中 ctxa 收到退出通知,会通知其绑定 work1,同时会广播给 ctxb 和 ctxc 绑定的 work2 和 work3。同

理,ctxb 收到退出通知,会通知到其绑定的 work2,同时会广播给 ctxc 绑定的 work3。

(2)、在构造 Context 的对象中不断地包裹 Context 实例形成一个引用关系链,这个关系链的方向是相反的,是自

底向上的。示例程序中多个 Context 对象的关系如下:

# 自底向上
ctxc.Context -->oc
ctxc.Context.Context -->ctxb
ctxc.Context.Context.cancelCtx-->ctxa
ctxc.Context.Context.cancelCtx.Context-->new(emptyCtx)//context.Background()

这个关系链主要用来切断当前 Context 实例和上层的 Context 实例之间的关系,比如 ctxb 调用了退出通知或定时

器到期了,ctxb 后续就没有必要在通知广播树上继续存在,它需要找到自己的 parent,然后执行

delete(parent.children,ctxb),把自己从广播树上清理掉。

整个关系链如图所示:

在这里插入图片描述

通过上文示例梳理出使用 Context 包的一般流程如下:

(1)、创建一个 Context 根对象。

func Background() Context
func TODO() Context

(2)、包装上一步创建的 Context 对象,使其具有特定的功能。

这些包装函数是 context package 的核心,几乎所有的封装都是从包装函数开始的。原因很简单,使用 context

包的核心就是使用其退出通知广播功能。

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

(3)、将上一步创建的对象作为实参传给后续启动的并发函数(通常作为函数的第一个参数),每个并发函数内部可以

继续使用包装函数对传进来的 Context 对象进行包装,添加自己所需的功能。

(4)、顶端的 goroutine 在超时后调用 cancel 退出通知函数,通知后端的所有 goroutine 释放资源。

(5)、后端的 goroutine 通过 select 监听 Context.Done() 返回的 chan,及时响应前端 goroutine 的退出通知,

一般停止本次处理,释放所占用的资源。

1.6 使用context传递数据的争议

该不该使用context传递数据:

首先要清楚使用 context 包主要是解决 goroutine 的通知退出,传递数据是其一个额外功能。可以使用它传递一

些元信息,总之使用 context 传递的信息不能影响正常的业务流程,程序不要期待在 context 中传递一些必需的

参数等,没有这些参数,程序也应该能正常工作。

在context中传递数据的坏处:

(1)、传递的都是 interface{} 类型的值,编译器不能进行严格的类型校验。

(2)、从 interface{} 到具体类型需要使用类型断言和接口查询,有一定的运行期开销和性能损失。

(3)、值在传递过程中有可能被后续的服务覆盖,且不易被发现。

(4)、传递信息不简明,较晦涩;不能通过代码或文档一眼看到传递的是什么,不利于后续维护。

context应该传递什么数据:

(1)、日志信息。

(2)、调试信息。

(3)、不影响业务主逻辑的可选数据。

context 包提供的核心的功能是多个 goroutine 之间的退出通知机制,传递数据只是一个辅助功能,应谨慎使用

context 传递数据。

1.7 Context常用案例

1.7.1 主协程主动调用cancel()取消子context

package main

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

func handler() {
	fmt.Println("handler start...", time.Now())
	ctx, cancel := context.WithCancel(context.Background())
	go do(ctx)
	// 子孙携程中的ctx会被取消
	cancel()
	time.Sleep(5 * time.Second)
	fmt.Println("handler end...", time.Now())
}

func do(ctx context.Context) {
	i := 1
	for {
		time.Sleep(1 * time.Second)
		select {
		case <-ctx.Done():
			fmt.Println("done", time.Now())
			return
		default:
			fmt.Printf("work %d seconds: %v\n", i, time.Now())
		}
		i++
	}
}

func main() {
	fmt.Println("main start...")
	handler()
	fmt.Println("main end...")
}
# 程序输出
main start...
handler start... 2023-02-06 12:18:29.3556787 +0800 CST m=+0.002016401
done 2023-02-06 12:18:30.3665436 +0800 CST m=+1.012881301
handler end... 2023-02-06 12:18:34.365822 +0800 CST m=+5.012159701
main end...

通过输出我们可以看出来,在主协程调用了 cancel() 之后,子协程中的 ctx 会被主动关闭掉,延迟时间是1秒,

会看到打印 done。

package main

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

func handler() {
	fmt.Println("handler start...", time.Now())
	ctx, _ := context.WithCancel(context.Background())
	go do(ctx)
	// 子孙携程中的ctx会被取消
	time.Sleep(5 * time.Second)
	fmt.Println("handler end...", time.Now())
}

func do(ctx context.Context) {
	i := 1
	for {
		time.Sleep(1 * time.Second)
		select {
		case <-ctx.Done():
			fmt.Println("done", time.Now())
			return
		default:
			fmt.Printf("work %d seconds: %v\n", i, time.Now())
		}
		i++
	}
}

func main() {
	fmt.Println("main start...")
	handler()
	fmt.Println("main end...")
}
# 程序输出
main start...
handler start... 2023-06-10 10:49:42.9315016 +0800 CST m=+0.002174101
work 1 seconds: 2023-06-10 10:49:43.9404127 +0800 CST m=+1.011085201
work 2 seconds: 2023-06-10 10:49:44.941524 +0800 CST m=+2.012196501
work 3 seconds: 2023-06-10 10:49:45.9422753 +0800 CST m=+3.012947801
work 4 seconds: 2023-06-10 10:49:46.9426627 +0800 CST m=+4.013335201
handler end... 2023-06-10 10:49:47.9421403 +0800 CST m=+5.012812801
main end...

从结果看出,我们如果不执行 cancel(),则会在设置的5秒睡眠时间内执行 work。

1.7.2 超时之后,调用cancle()

package main

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

func handler() {
	fmt.Println("handler start...", time.Now())
	// 设置2秒的超时时间
	ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)
	go do(ctx)
	time.Sleep(5 * time.Second)
	fmt.Println("handler end...", time.Now())
}

func do(ctx context.Context) {
	i := 1
	for {
		time.Sleep(1 * time.Second)
		select {
		case <-ctx.Done():
			fmt.Println("done", time.Now())
			return
		default:
			fmt.Printf("work %d seconds: %v\n", i, time.Now())
		}
		i++
	}
}

func main() {
	fmt.Println("main start...")
	handler()
	fmt.Println("main end...")
}
# 程序输出
main start...
handler start... 2023-06-10 10:51:17.0507657 +0800 CST m=+0.002568201
work 1 seconds: 2023-06-10 10:51:18.0618317 +0800 CST m=+1.013634201
done 2023-06-10 10:51:19.0620425 +0800 CST m=+2.013845001
handler end... 2023-06-10 10:51:22.0630702 +0800 CST m=+5.014872701
main end...

通过输出可以看出来,在2s超时之后,也就是done会主动打印出来,表明 cancel() 被主动调用了。

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

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

相关文章

ResNet

论文信息 论文名称&#xff1a;Deep Residual Learning for Image Recognition 论文地址&#xff1a;https://arxiv.org/pdf/1512.03385.pdf 发表期刊&#xff1a;CVPR 发表年份&#xff1a;2016 主要问题 在引言中作者提出了一个问题&#xff1a;训练一个更好的网络是否像堆…

这个网站,多希望你早点知道,越早越好!

这是一个有趣、神奇的个人博客网站。 这是一个马斯克经常上的网站&#xff0c;而且马斯克还在推特上关注了这个网站的账号。 网站地址&#xff1a;https://waitbutwhy.com/ 这个网站上的内容并不多&#xff0c;网站2013年创建的&#xff0c;至今已有10年&#xff0c;一共才产出…

python爬虫工程师,如何从零开始部署Scrapyd+Feapder+Gerapy?

突然被告知要连着上整整十一天的班&#xff0c;有一点点累&#xff0c;简单更新一下内容吧&#xff0c;水个积分 关注公众号&#xff1a;python技术训练营&#xff0c;精选优质文档&#xff0c;好玩的项目 内容&#xff1a; 1.面试专题几十个大厂面试题 2.入门基础教程 3.11模块…

活动邀请函五秒钟下载即用

在日常中&#xff0c;人们都是以纸质的邀请函发送给被邀请者&#xff0c;不仅需要花费大量的精力和时间去书写发送活动邀请函&#xff0c;还存在着被邀请人没有及时收到活动邀请函而错过参与的时间等。而这样只需制作一份就可以全网分享&#xff0c;用户短时间就能收到活动邀请…

可变参数列表

"多少人都&#xff0c;生来纯洁完美&#xff0c;心底从不染漆黑。" 我们想要实现一个函数&#xff0c;这个函数的功能是返回一个整形的最大值。 emm&#xff0c;似乎有那点味道。但这应用场景似乎很受限制&#xff0c;因为这个函数比较的有效区间&#xff0c;只能装下…

Pycharm远程开发之全局pip,激活远程虚拟环境pip,以及pip的--user选项

前言 最近需要部署一下生成对抗网络的开发环境&#xff0c;我自己的笔记本没有带显卡&#xff0c;想到实验室的服务器有带显卡索性就用实验室服务器的环境开发&#xff0c;通过pycharm的远程开发功能连接到服务器&#xff0c;本来以为轻轻松松就可以开始写代码了&#xff0c;结…

springcloud整合nacos

1.订单服务&#xff08;order&#xff09; 1.1 安装nocas Nacos 快速开始 --注意&#xff1a;nacos 我的是 nacos-server-1.4.1.tar.gz 1.2 新建order-nacos 模块 1.3 修改pom文件 添加 nacos 依赖 1.4 配置文件添加 nacos 地址 1.5 创建启动类 使用 RestTemplate 方式调用服…

字符设备驱动内部实现

只要文件存在&#xff0c;就会有唯一对应的inode号&#xff0c;且相应的会存在一个struct inode结构体.,在应用层通过open&#xff08;&#xff09;打开一个设备文件&#xff0c;会对应产生一个inode号&#xff0c;通过inode号可以找到文件的inode结构体&#xff0c;inode结构体…

中介者模式(二十一)

相信自己&#xff0c;请一定要相信自己 上一章简单介绍了观察者模式(二十), 如果没有看过, 请观看上一章 一. 中介者模式 引用 菜鸟教程里面中介者模式介绍: https://www.runoob.com/design-pattern/mediator-pattern.html 中介者模式&#xff08;Mediator Pattern&#xff…

paddlespeech http服务解决输出无符号

1.前情提要 下载paddlespeech官网代码并运行http服务进行中文识别时&#xff0c;会发现选择某些模型&#xff08;我用的是conformer_wenetspeech&#xff09;&#xff0c;是别的结果为一串文字&#xff0c;没有标点&#xff0c;效果如下&#xff1a; 经过调用padddlespeech相关…

【裸机开发】使用汇编清除 .bss 段

目录 1、为什么要清除 .bss 段 2、使用汇编清除 .bss 段 1、为什么要清除 .bss 段 .bss 段保存的是 未被初始化 或者 初始化为0 的全局/静态变量。在编译器看来&#xff0c;这些东西是多余的&#xff0c;实际并不会给他们分配空间。因此&#xff0c;编译生成目标文件的时候&…

38.SpringCloud—注册中心(eureka/nacos)、负载均衡Ribbon

目录 一、SpringCloud。 &#xff08;1&#xff09;认识微服务。 &#xff08;1.1&#xff09;单体架构与分布式架构&#xff08;微服务&#xff09;。 &#xff08;1.2&#xff09;微服务技术对比。 &#xff08;1.3&#xff09;SpringCloud。 &#xff08;2&#xff09…

linux创建静态库

创建一个目录&#xff0c;比如今天是6月13号&#xff0c;就mkdir 0613(创建目录0613),然后cd 0613&#xff08;进入0613目录&#xff09; 1.创建C语言库函数文件myheight.c vi myhight.c 2.创建C语言函数文件myweight.c vi myweight.c 3.创建C语言函数文件age.c vi myage.c 4…

代码审计-Java项目JDBCMybatisHibernate注入预编译写法

文章目录 Javaweb-数据库操作-模式&写法&预编译等环境搭建JDBC 注入分析关于预编译 Mybatis 注入分析Hibernate 注入分析总结&#xff1a; Javaweb-代码审计SQL注入-INXEDU在线网校 Javaweb-数据库操作-模式&写法&预编译等 环境搭建 VulDemo审计源码百度云 在…

深入理解深度学习——Transformer:解码器(Decoder)部分

分类目录&#xff1a;《深入理解深度学习》总目录 相关文章&#xff1a; 注意力机制&#xff08;Attention Mechanism&#xff09;&#xff1a;基础知识 注意力机制&#xff08;Attention Mechanism&#xff09;&#xff1a;注意力汇聚与Nadaraya-Watson核回归 注意力机制&…

微信小程序分享到微信,公众号h5分享到微信,微信小程序跳转h5页面

一&#xff1a;微信小程序分享到微信 1&#xff1a;需求 分享微信小程序中某个详情页&#xff0c;可以分享到群&#xff0c;个人&#xff0c;朋友圈&#xff0c;好友点击分享页&#xff0c;能跳转到对应详情页阅读。 2&#xff1a;分析问题 如何实现分享&#xff1f;分享时如…

Nvidia 3060显卡 CUDA环境搭建(Ubuntu22.04+Nvidia 510+Cuda11.6+cudnn8.8)

写在前面 工作中遇到&#xff0c;简单整理理解不足小伙伴帮忙指正 对每个人而言&#xff0c;真正的职责只有一个&#xff1a;找到自我。然后在心中坚守其一生&#xff0c;全心全意&#xff0c;永不停息。所有其它的路都是不完整的&#xff0c;是人的逃避方式&#xff0c;是对大…

【踩坑记录】STC8H8K64U硬件PWM使用小结

快速导航 写在前面库配置时钟配置GPIO配置定时器配置串口配置硬件PWM特殊功能同步功能 总结 写在前面 不出意外这是我第一次也是最后一次使用STC的芯片&#xff0c;写这篇博的目的纯粹记录下前段时间调试的痛苦经历&#xff0c;所有目前打算选或是已经开始调试这款芯片的朋友&…

Ubuntu16.04部署BEVformer 实时记录

一 配置依赖 a. Create a conda virtual environment and activate it. conda create -n open-mmlab python3.8 -y conda activate open-mmlabb. Install PyTorch and torchvision following the official instructions. pip install torch1.9.1cu111 torchvision0.10.1cu11…

【Linux】面试重点:死锁和生产消费模型原理

面试要点来了~ 文章目录 前言一、死锁的一系列问题二、生产者消费者模型原理总结 前言 上一篇的互斥量原理中我们讲解了锁的原理&#xff0c;我们知道每次线程申请锁的时候一旦申请成功这个线程自己就把锁带在自己身上了&#xff0c;这就保证了锁的原子性&#xff08;因为只有…