go并发处理业务

news2024/11/29 8:40:37

引言

实际上,在服务端程序开发和爬虫程序开发时,我们的大多数业务都是IO密集型业务,什么是IO密集型业务,通俗地说就是CPU运行时间只占整个业务执行时间的一小部分,而剩余的大部分时间都在等待IO操作。

IO操作包括http请求、数据库查询、文件读取、摄像设备录音设备的输入等等。这些IO操作会引起中断,使业务线程暂时放弃cpu,暂时停止运行,等待IO操作完成后在重新获得cpu、继续运行下去。

比如下面这段代码,我执行了一个很简单的IO操作,就是请求百度的主页面。在IO操作之后我又执行了一个一千万数量级的循环,用来模拟cpu计算业务。

func main() {
	t := time.Now().UnixMilli()

	// 发起http请求病接收响应
	res, _ := http.Get("https://www.baidu.com")
	fmt.Printf("https请求结束,耗时%dms\n", time.Now().UnixMilli()-t)

	// 进行1e7次计算操作,用来模拟业务处理时cpu计算内容
	body, _ := io.ReadAll(res.Body)
	res.Body.Close()
	for i := 1; i <= 1e7; i++ {
		_ = i * i
		_ = i + i
	}
	
	fmt.Printf("程序运行结束,总耗时%dms, 数据长度为%d", time.Now().UnixMilli()-t, len(body))
}

这段程序在我的电脑上的输出结果是:

https请求结束,耗时250ms
程序运行结束,总耗时256ms, 数据长度为227

不难看出,向百度发起请求耗费了250ms,而一千万次cpu计算仅耗费6ms。(实际情况中,大多数业务的cpu计算量甚至远远达不到1e7级别)

我们为什么要引入并发呢,正如操作系统课程上讲的那样,目的就是为了提高cpu的利用率,如果某个线程正处在等待IO的状态,此时cpu是空闲的,那么我们就应该用cpu去执行别的线程,而不是傻傻的等待这个进程结束再去进行别的操作。

并发样例

假设现在有一个爬虫项目,它的业务流程如下图所示,我大致描述一下流程:

  1. 首先要爬取A和B的页面,并解析页面结果
  2. 然后根据B页面的解析结果,设定好相关参数,对C、D、F页面进行爬取;同样的根据A页面的解析结果,设定好相关参数,对E页面进行爬取
  3. 接下来根据C、D页面的解析结果,设定好相关参数,对G页面进行爬取
  4. 最后,对E、F、G三个页面进行解析,得到我们需要的最终数据

虽然只是假设,但类似的业务流程在爬虫项目中是很常见的。
除此之外,类似的业务流程在后端程序开发中也很常见,对于前端的一个请求,后端很可能需要多次访问数据库、向下游服务器发送请求、访问内存外的缓存数据等等。
在这里插入图片描述
每个页面爬取耗费的时间如图中所示,由于cpu计算耗费时间很短,所以在这里就忽略不计。我们用sleep函数来模拟发起请求耗费的时间,代码如下图所示,每个task函数代表爬取一个页面:

func taskA() string {
	time.Sleep(time.Millisecond * 40)
	return "AAA"
}
func taskB() string {
	time.Sleep(time.Millisecond * 30)
	return "BBB"
}
func taskC(resultOfB string) string {
	time.Sleep(time.Millisecond * 30)
	return "CCC"
}
func taskD(resultOfB string) string {
	time.Sleep(time.Millisecond * 30)
	return "DDD"
}
func taskF(resultOfB string) string {
	time.Sleep(time.Millisecond * 30)
	return "FFF"
}
func taskE(resultOfA string) string {
	time.Sleep(time.Millisecond * 30)
	return "EEE"
}
func taskG(resultOfC, resultOfD string) string {
	time.Sleep(time.Millisecond * 30)
	return "GGG"
}

串行

串行,也就是不使用并发的代码如下所示:
串行的代码非常好写,从前往后把task函数顺序执行就行了,之所以在这里把串行代码和运行结果列出了,主要是为了和下面的并发做对比。


func main() {
	t := time.Now().UnixMilli()

	// 按照任务执行的先后要求,顺序执行所有任务
	resultOfA := taskA()
	fmt.Printf(" %dms: A over\n", time.Now().UnixMilli()-t)

	resultOfB := taskB()
	fmt.Printf(" %dms: B over\n", time.Now().UnixMilli()-t)

	resultOfC := taskC(resultOfB)
	fmt.Printf(" %dms: C over\n", time.Now().UnixMilli()-t)

	resultOfD := taskD(resultOfB)
	fmt.Printf(" %dms: D over\n", time.Now().UnixMilli()-t)

	resultOfE := taskE(resultOfA)
	fmt.Printf(" %dms: E over\n", time.Now().UnixMilli()-t)

	resultOfF := taskF(resultOfB)
	fmt.Printf(" %dms: F over\n", time.Now().UnixMilli()-t)

	resultOfG := taskG(resultOfC, resultOfD)
	fmt.Printf(" %dms: G over\n", time.Now().UnixMilli()-t)
	
	// 打印E、F、G的运行结果,至此程序就运行结束了,打印程序运行所耗费的时间
	fmt.Printf(" %dms: all over, %s, %s, %s\n", time.Now().UnixMilli()-t, resultOfE, resultOfF, resultOfG)
}

程序的执行结果如下所示,可以看到效率很慢很慢,总执行时间等于所有任务执行时间之和。

41ms: A over
73ms: B over
118ms: C over
164ms: D over
194ms: E over
240ms: F over
285ms: G over
286ms: all over, EEE, FFF, GGG

并发

本文的重点来了,如何并发地执行上面假设的爬虫程序?怎样才能让cpu的利用效率最高?

go为我们提供了非常好用的goroutine和chan,前者叫做协程也可以简单地认为是小型线程,能够以极小的开销和极快的速度启动一个并发任务;后者叫做通道,也可以叫管道,是协程和协程间通信的工具,不仅能够传递数据,还能够阻塞和唤醒协程从而实现协程间的同步。

在下面的代码中,我们使用通道来进行任务与任务之间的通信,我们为每一个任务都开一个协程,如果该任务没有前置依赖,那么就之间执行,然后把执行结果放到对应的通道中;如果该任务有前置依赖任务,那么先从通道中读取自己所需要的数据,然后再执行相应任务。

代码如下所示,逻辑很简单,主要是体现了一种并发的思想和并发执行任务的思路,现实情况中并发业务的代码肯定要复杂得多。

func main() {
	t := time.Now().UnixMilli()

	// AtoE代表A把自己的运行结果交给E的所经过的通道,下面同理
	AtoE := make(chan string, 1)
	BtoC := make(chan string, 1)
	BtoF := make(chan string, 1)
	BtoD := make(chan string, 1)
	CtoG := make(chan string, 1)
	DtoG := make(chan string, 1)
	GtoEnd := make(chan string, 1)
	FtoEnd := make(chan string, 1)
	EtoEnd := make(chan string, 1)

	// 每个go func代表着给某个任务开一个协程
	// 为A任务开个协程
	go func() {
		resultOfA := taskA()
		AtoE <- resultOfA
		fmt.Printf(" %dms: A over\n", time.Now().UnixMilli()-t)
	}()

	// 为B任务开个协程
	go func() {
		resultOfB := taskB()
		BtoF <- resultOfB
		BtoC <- resultOfB
		BtoD <- resultOfB
		fmt.Printf(" %dms: B over\n", time.Now().UnixMilli()-t)
	}()

	// 为C任务开个协程
	go func() {
		resultOfB := <-BtoC
		resultOfC := taskC(resultOfB)
		CtoG <- resultOfC
		fmt.Printf(" %dms: C over\n", time.Now().UnixMilli()-t)
	}()

	// 为D任务开个协程
	go func() {
		resultOfB := <-BtoD
		resultOfD := taskC(resultOfB)
		DtoG <- resultOfD
		defer fmt.Printf(" %dms: D over\n", time.Now().UnixMilli()-t)
	}()

	// 为F任务开个协程
	go func() {

		resultOfB := <-BtoF
		resultOfF := taskF(resultOfB)
		FtoEnd <- resultOfF
		fmt.Printf(" %dms: F over\n", time.Now().UnixMilli()-t)
	}()

	// 为E任务开个协程
	go func() {
		resultOfA := <-AtoE
		resultOfE := taskC(resultOfA)
		EtoEnd <- resultOfE
		fmt.Printf(" %dms: E over\n", time.Now().UnixMilli()-t)
	}()

	// 为G任务开个协程
	go func() {
		resultOfC := <-CtoG
		resultOfD := <-DtoG
		resultOfG := taskG(resultOfC, resultOfD)
		GtoEnd <- resultOfG
		fmt.Printf(" %dms: G over\n", time.Now().UnixMilli()-t)
	}()

	// 接收E、F、G的运行结果,至此程序就运行结束了,打印程序运行所耗费的时间
	resultOfE, resultOfF, resultOfG := <-EtoEnd, <-FtoEnd, <-GtoEnd
	fmt.Printf(" %dms: all over, %s, %s, %s\n", time.Now().UnixMilli()-t, resultOfE, resultOfF, resultOfG)
}

程序的执行结果如下所示,可以看到程序运行中花费了105ms,很接近关键路径的长度90ms,说明我们这个程序的并发性很好,在最大程度上实现了cpu的高效利用。(PS:关键路径begin → B→ C → G → end)

43ms: A over
44ms: B over
74ms: E over
74ms: C over
74ms: D over
74ms: F over
105ms: G over
105ms: all over, CCC, FFF, GGG

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

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

相关文章

uniapp 触底加载

方式一 onReachBottomDistance 缺点&#xff1a;需要整个页面滑动&#xff0c;局部滑动触发不了 { // pages.json // 路由下增加 onReachBottomDistance "path": "detailed/detailed","style": {"navigationBarTitleText": "收…

cpu温度监测 Turbo Boost Switcher Pro for mac最新

Turbo Boost Switcher Pro是一款Mac电脑上的应用程序&#xff0c;旨在帮助用户控制和管理CPU的Turbo Boost功能。Turbo Boost是Intel处理器中的一项技术&#xff0c;可以在需要更高性能时自动提高处理器的频率。然而&#xff0c;这可能会导致电池消耗更快和温度升高。 以下是T…

《计算机视觉中的多视图几何》笔记(2)

Projective Geometry and Transformations of 2D 本章主要介绍本书必要的几何知识与符号。 文章目录 Projective Geometry and Transformations of 2D2.1 Planar geometry2.2 The 2D projective plane2.2.1 Points and lines 2.2.2 Ideal points and the line at infinity2.2…

elementui 中 DateTimePicker 组件时间自定义格式化

elementui 中 DateTimePicker 组件时间自定义格式化 需求分析 需求 elementui 中 DateTimePicker 组件时间自定义格式化 自定义需求&#xff1a;需要获取到 DateTimePicker 组件时间的值为[“2023/9/5 20:2”,“2023/9/4 2:10”] 分析 源码如下&#xff1a; <el-date-pick…

TypeScript类型兼容:协变和逆变

&#x1f3ac; 岸边的风&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 引言 协变&#xff08;Covariance&#xff09; 协变&#xff1a;类型的向下兼容性 逆变&#xff08;Contravaria…

deepspeed训练报错torch.distributed.elastic.multiprocessing.errors.ChildFailedError

测试场景&#xff1a;使用deepspeed框架训练gpt模型 问题&#xff1a; 报错torch.distributed.elastic.multiprocessing.errors.ChildFailedError 具体见截图&#xff1a;

随手笔记(四十五)——idea git冲突

图片为引用&#xff0c;在一次导入项目至gitee的过程中&#xff0c;不知道为什么报了403&#xff0c;很奇怪的一个错误&#xff0c;网上很多的答案大概分成两种。 第一种是最多的&#xff0c;直接找到windows凭据删掉 很抱歉的告诉各位&#xff0c;你们很多人到这里就已经解…

线性代数的本质(四)——行列式

文章目录 行列式二阶行列式 n n n 阶行列式行列式的性质克拉默法则行列式的几何理解 行列式 二阶行列式 行列式引自对线性方程组的求解。考虑两个方程的二元线性方程组 { a 11 x 1 a 12 x 2 b 1 a 21 x 1 a 22 x 2 b 2 \begin{cases} a_{11}x_1a_{12}x_2b_1 \\ a_{21}x_…

Vue3:proxy数据取值proxy[Target]取值

vue3底层是使用proxy进行代理的&#xff0c;而proxy中[[Target]]才是想要的值。 获取target值的方式一&#xff1a; <script setup>//先引入toRawimport { toRaw } from vue;//再使用console.log(toRaw(数据名))</script> 获取target值的方式二&#xff1a; <…

阿里云服务器配置怎么选择?几核几G?带宽系统盘怎么选?

阿里云服务器配置选择_CPU内存/带宽/存储配置_小白指南&#xff0c;阿里云服务器配置选择方法包括云服务器类型、CPU内存、操作系统、公网带宽、系统盘存储、网络带宽选择、安全配置、监控等&#xff0c;阿小云分享阿里云服务器配置选择方法&#xff0c;选择适合自己的云服务器…

NetSuite知识会汇编-管理员篇顾问篇2023

本月初&#xff0c;开学之际&#xff0c;我们发布了《NetSuite知识会汇编-用户篇 2023》&#xff0c;这次发布《NetSuite知识会汇编-管理员篇&顾问篇2023》。本篇挑选了近两年NetSuite知识会中的一些文章&#xff0c;涉及开发、权限、系统管理等较深的内容&#xff0c;共19…

嵌入式Linux驱动开发(I2C专题)(二)

I2C系统的重要结构体 参考资料&#xff1a; Linux驱动程序: drivers/i2c/i2c-dev.cI2CTools: https://mirrors.edge.kernel.org/pub/software/utils/i2c-tools/ 1. I2C硬件框架 2. I2C传输协议 3. Linux软件框架 4. 重要结构体 使用一句话概括I2C传输&#xff1a;APP通过I2…

linux rz乱码文件删除

通过rz上传文件的时候经常会遇到 文件乱码问题&#xff0c;删又删不掉。 使用rz -be的方法上传 遇到乱码文件操作步骤 1. ls -i # 列出文件的编号 2. find . -inum 29229139 -delete # 根据编号删除文件 find 需要扫描的路径 -inum 文件编号 -delete

VRTK4⭐四.和 UI 元素交互

文章目录 &#x1f7e5; 安装Tilia Unity.UI&#x1f7e7; 配置射线与UI交互器1️⃣ 配置直线射线2️⃣ 配置UI交互器 &#x1f7e8; 配置UI1️⃣ 更新EventSystem2️⃣ 进行Canvas设置 我们要实现的功能: 右手触摸到圆盘:显示直线射线 右手圆盘键按下:与选中UI交互 &#x1f7…

JWT安全

文章目录 理论知识cookie(放在浏览器)session(放在 服务器)tokenjwt&#xff08;json web token&#xff09;headerpayloadSignatureJWT通信流程 JWT与Token 区别相同点区别 WebGoat靶场--JWT tokens环境启动第四关第五关第七关 属于越权漏洞 理论知识 cookie(放在浏览器) ​…

静电消除器在工业设备中的用途

静电消除有几种&#xff0c;其中包括离子风枪、离子风嘴、离子风棒、离子风枪、离子风蛇等。今天我们就来聊一下离子风蛇。 离子风蛇是一种坐立式静电消除器&#xff0c;可消除难以接近或接触物体的静电或灰尘&#xff0c;可将蛇管随意变形使风咀指向目标方位&#xff0c;无需…

AIGC:【LLM(八)】——Baichuan2技术报告

文章目录 摘要1. 引言2. 预训练2.1 预训练数据&#xff08;Pre-training Data&#xff09;2.2 架构&#xff08;Architecture&#xff09;2.3 令牌化器&#xff08;Tokenizer&#xff09;2.3.1 Positional Embeddings 2.4 激活和规范化&#xff08;Activations and Normalizati…

vue2-x6-dag自定义vue组件节点

效果如图 官方案例 人工智能建模 DAG 图 vue2中自定义节点 代码 1.dag.json [{"id": "1","shape": "dag-node","x": 290,"y": 110,"data": {"label": "读数据","status&q…

2023CVPR:图像恢复的又一力作

今天要分享的论文是2023CVPR《Efficient and Explicit Modelling of Image Hierarchies for Image Restoration》&#xff0c;图像恢复领域的又一力作&#xff0c;提供了新的发现&#xff0c;给出了新的解决方案 代码 https://github.com/ofsoundof/GRL-Image-Restoration 问…

C语言指针快速入门

指针的基本介绍 简单的说指针用于表示地址&#xff0c;存放的是一个地址 获取指针的地址 //指针的入门 #include <stdio.h>int main() {int num 1;//num的地址是多少//说明1&#xff1a;如果要输出一个变量的地址使用的格式是%p//说明2&#xff1a;&num 表示取出n…