go并发处理业务

引言

实际上,在服务端程序开发和爬虫程序开发时,我们的大多数业务都是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 <- resultOfAfmt.Printf(" %dms: A over\n", time.Now().UnixMilli()-t)}()// 为B任务开个协程go func() {resultOfB := taskB()BtoF <- resultOfBBtoC <- resultOfBBtoD <- resultOfBfmt.Printf(" %dms: B over\n", time.Now().UnixMilli()-t)}()// 为C任务开个协程go func() {resultOfB := <-BtoCresultOfC := taskC(resultOfB)CtoG <- resultOfCfmt.Printf(" %dms: C over\n", time.Now().UnixMilli()-t)}()// 为D任务开个协程go func() {resultOfB := <-BtoDresultOfD := taskC(resultOfB)DtoG <- resultOfDdefer fmt.Printf(" %dms: D over\n", time.Now().UnixMilli()-t)}()// 为F任务开个协程go func() {resultOfB := <-BtoFresultOfF := taskF(resultOfB)FtoEnd <- resultOfFfmt.Printf(" %dms: F over\n", time.Now().UnixMilli()-t)}()// 为E任务开个协程go func() {resultOfA := <-AtoEresultOfE := taskC(resultOfA)EtoEnd <- resultOfEfmt.Printf(" %dms: E over\n", time.Now().UnixMilli()-t)}()// 为G任务开个协程go func() {resultOfC := <-CtoGresultOfD := <-DtoGresultOfG := taskG(resultOfC, resultOfD)GtoEnd <- resultOfGfmt.Printf(" %dms: G over\n", time.Now().UnixMilli()-t)}()// 接收E、F、G的运行结果,至此程序就运行结束了,打印程序运行所耗费的时间resultOfE, resultOfF, resultOfG := <-EtoEnd, <-FtoEnd, <-GtoEndfmt.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.rhkb.cn/news/131516.html

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

相关文章

反编译小程序详细教程,处理各种异常报错

文章目录 一、准备工作 &#xff08;一&#xff09;安装Nodejs &#xff08;二&#xff09;解密和逆向工具 二、小程序缓存文件解密 &#xff08;一&#xff09;定位小程序缓存路径 &#xff08;二&#xff09;源码解密 &#xff08;三&#xff09;源码反编译 三、小结 四、异常…

Go 异常处理

代码在执行的过程中可能因为一些逻辑上的问题而出现错误 func test1(a, b int) int {result : a / breturn result } func main() {resut : test1(10, 0)fmt.Println(resut) }panic: runtime error: integer divide by zero goroutine 1 [running]: …

华为云arm架构的linux系统中通过docker部署python环境

背景 有时候需要在无互联网的环境安装部署python环境,虽然可以在linux系统中直接安装python环境,但是比较复杂乱,特别是环境多的时候,其实可以通过docker打包安装的方式来实现 1、租用华为云arm加载的服务器 https://www.huaweicloud.com/product/ecs.html 2、安装doc…

请明星出席品牌周年庆活动:巧妙策略与成功之道

品牌的周年庆典是一次展示实力、感谢客户和吸引更多关注的机会。在这个特殊时刻&#xff0c;让明星出席活动演出无疑是让庆典更加引人注目和难忘的方式。明星的存在不仅能增加活动的知名度&#xff0c;还能为品牌增色不少。然而&#xff0c;邀请明星出席活动是一项复杂的任务&a…

phpcmsV9.6.0sql注入漏洞分析

目录 前言 环境准备 漏洞点 看一看parse_str函数 看一看sys_auth函数 看一看get_one函数 全局搜索sys_auth($a_k, ENCODE) 查看哪里调用了 set_cookie 查看safe_replace函数 判断登录绕过 index的业务 加载modules/wap/index.php 加载modules/attachment/attachme…

免费版Photoshop2024智能人像磨皮插件

Portraiture是一款智能磨皮插件&#xff0c;为Photoshop和Lightroom添加一键磨皮美化功能&#xff0c;快速对照片中皮肤、头发、眉毛等部位进行美化&#xff0c;无需手动调整&#xff0c;大大提高P图效率。全新4版本&#xff0c;升级AI算法&#xff0c;并独家支持多人及全身模式…

vue手写提示组件弹窗

1、弹框展示 2、message组件 新建一个message.vue <template><div class"wrapper" v-if"isShow" :class"showContent ? fadein : fadeout">{{ text }}</div> </template> <script></script> <style s…

微信怎么定时发圈?

定时发圈的妙用 在合适的时间点发布新的产品、促销活动&#xff0c;不仅能够及时提醒用户品牌的存在&#xff0c;还可以引发用户的兴趣&#xff0c;增加品牌的曝光率。 选择最佳的发朋友圈时间段&#xff0c;以确保推广内容得到最大的曝光和关注&#xff0c;提高广告投放的效果…

python元组

元组tumple tumple_python1.元组的创建2.元组是不可变序列3.元组的遍历 tumple_python -元组(tuple)是是一个有序、不可变的序列。元组用小括号 () 表示&#xff0c;其中的元素可以是任意类型&#xff0c;并且可以通过索引访问。 不可变序列与可变序列 1>不可变序列&#x…

MySQL中分区与分表的区别

MySQL中分区与分表的区别 一、分区与分表的区别 分区和分表是在处理大规模数据时的两种技术手段&#xff0c;尽管它们的目标都是提升系统的性能和数据管理的效率&#xff0c;但它们的实现方式和应用场景略有不同。 1. 分区 分区是将一个大表分割为多个更小的子表&#xff0c…

【WEB3】区块链开发入门项目之「简易NFT交易市场」

参考文章 教程英文版&#xff1a;https://docs.alchemy.com/docs/how-to-build-an-nft-marketplace-from-scratch 教程中文版&#xff1a;https://zhuanlan.zhihu.com/p/557479922?utm_id0 以太坊测试币领取&#xff1a;https://goerlifaucet.com/ 入门项目地址&#xff1…

【C++杂货铺】优先级队列的使用指南与模拟实现

文章目录 一、priority_queue的介绍二、priority_queue的使用2.1 数组中的第k个最大元素 三、priority_queue模拟实现3.1 仿函数3.2 成员变量3.3 成员函数3.3.1 构造函数3.3.2 AdjustDown3.3.3 push3.3.4 AdjustUp3.3.5 pop3.3.6 empty3.3.7 size 四、结语 一、priority_queue的…

苏州融资融券(两融)开户利率最低能做到多少?5%!

两融利率是指投资者在证券交易所通过融资买入和融券卖出股票时所支付的利息费用。两融包括融资和融券两部分&#xff0c;而融资和融券则是两种常见的金融杠杆运作方式。融资是指投资者通过向券商借入资金来买入股票。在融资中&#xff0c;投资者需要支付给券商一定的利息费用。…

面了一个测试工程师要求月薪26K,总感觉他背了很多面试题...

最近有朋友去华为面试&#xff0c;面试前后进行了20天左右&#xff0c;包含4轮电话面试、1轮笔试、1轮主管视频面试、1轮hr视频面试。 据他所说&#xff0c;80%的人都会栽在第一轮面试&#xff0c;要不是他面试前做足准备&#xff0c;估计都坚持不完后面几轮面试。 其实&…

Linux系统离线安装RabbitMQ

安装rabbitmq 1、下载安装包 首先进入官网进行安装包的下载&#xff0c;在下载时一定要注意erlong版本和rabbitmq-server版本匹配 rabbitmq版本对应关系&#xff1a;传送门 Erlong下载地址:传送门 rabbitmq-server下载地址:传送门 socat 不同版本 centos7:传送门 cent…

【Unity3D】UI Toolkit数据动态绑定

1 前言 本文将实现 cvs 表格数据与 UI Toolkit 元素的动态绑定。 如果读者对 UI Toolkit 不是太了解&#xff0c;可以参考以下内容。 UI Toolkit简介UI Toolkit容器UI Toolkit元素UI Toolkit样式选择器UI Toolkit自定义元素 本文完整资源见→UI Toolkit数据动态绑定。 2 数据…

【办公软件】微信占用C盘空间如何释放

C盘又满了&#xff0c;如果微信QQ用得多的话&#xff0c;很可能微信就占用了很多空间。当然就算安装目录选择到了其他盘&#xff0c;但是软件的缓存也有可能占用C盘。 因为微信或是其他常用软件的缓存路径都会默认保存在C盘&#xff0c;随着传输文件较多&#xff0c;每次都会自…

编码转换流

同理&#xff0c;创建f1和f2方法&#xff0c;分别测试OutputStreamWriter和InputStreamReader 也是主要分三步&#xff0c;即1创建流 2使用流 3关流 OutputStreamWriter f1方法 因为要操作流&#xff0c;所以先创建一个try-catch-finally结构&#xff0c;创建流对象Out…

ARTS 打卡 第一周,初试ARTS

前言 认识三掌柜的想必都知道&#xff0c;我持续创作技术博客已经有6年时间了&#xff0c;固定每个月发布不少于6篇博文。同时&#xff0c;自己作为一名热爱分享的开发者&#xff0c;像ARTS这样的活动自然少不了我。由于我是打算挤在一起分享&#xff0c;之前都是做了本地文档记…

在PHP8中统计数组元素个数-PHP8知识详解

在php8中&#xff0c;统计数组元素的个数&#xff0c;有下面几个函数&#xff1a;使用count()函数统计数组元素个数、使用sizeof()函数统计数组元素个数。还讲到了&#xff0c;使用array_count_values()函数来统计数组中每个元素出现的次数。 1、使用count()函数统计数组元素个…