论go语言中goroutine的使用

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介:

go中的goroutine是go语言在语言级别支持并发的一种特性。初接触go的时候对go的goroutine的欢喜至极,实现并发简便到简直bt的地步。但是在项目过程中,越来越发现goroutine是一个很容易被大家滥用的东西。goroutine是一把双面刃。这里列举一下goroutine使用的几宗罪:

1 goroutine的指针传递是不安全的

1
2
3
4
5
6
7
8
fun main() {
     request := request.NewRequest()  //这里的NewRequest()是传递回一个type Request的指针
     go saveRequestToRedis1(request)
     go saveReuqestToRedis2(request)
     
     select {}
 
}

非常符合逻辑的代码:

主routine开一个routine把request传递给saveRequestToRedis1,让它把请求储存到redis节点1中

同时开另一个routine把request传递给saveReuqestToRedis2,让它把请求储存到redis节点2中

然后主routine就进入循环(不结束进程)

 

问题现在来了,saveRequestToRedis1和saveReuqestToRedis2两个函数其实不是我写的,而是团队另一个人写的,我对其中的实现一无所知,也不想去仔细看内部的具体实现。但是根据函数名,我想当然地把request指针传递进入。

 

好了,实际上saveRequestToRedis1和saveRequestToRedis2 是这样实现的:

1
2
3
4
5
6
7
func saveRequestToRedis1(request *Request){
     
      request.ToUsers = [] int {1,2,3}  //这里是一个赋值操作,修改了request指向的数据结构
     
     redis.Save(request)
     return
}

这样有什么问题?saveRequestToRedis1和saveReuqestToRedis2两个goroutine修改了同一个共享数据结构,但是由于routine的执行是无序的,因此我们无法保证request.ToUsers设置和redis.Save()是一个原子操作,这样就会出现实际存储redis的数据错误的bug。

 

好吧,你可以说这个saveRequestToRedis的函数实现的有问题,没有考虑到会是使用go routine调用。请再想一想,这个saveRequestToRedis的具体实现是没有任何问题的,它不应该考虑上层是怎么使用它的。那就是我的goroutine的使用有问题,主routine在开一个routine的时候并没有确认这个routine里面的任何一句代码有没有修改了主routine中的数据。对的,主routine确实需要考虑这个情况。但是按照这个思路,所以呢?主goroutine在启用go routine的时候需要阅读子routine中的每行代码来确定是否有修改共享数据??这在实际项目开发过程中是多么降低开发速度的一件事情啊!

 

go语言使用goroutine是想减轻并发的开发压力,却不曾想是在另一方面增加了开发压力。

 

上面说的那么多,就是想得出一个结论:

gorotine的指针传递是不安全的!!

 

如果上一个例子还不够隐蔽,这里还有一个例子:

1
2
3
4
5
6
7
8
fun ( this  *Request)SaveRedis() {
     redis1 := redis.NewRedisAddr( "xxxxxx" )
     redis2 := redis.NewRedisAddr( "xxxxxx" )
     go  this .saveRequestToRedis(redis1)
     go  this .saveRequestToRedis(redis2)
     
     select {}
}

很少人会考虑到this指针指向的对象是否会有问题,这里的this指针传递给routine应该说是非常隐蔽的。

 

2 goroutine增加了函数的危险系数

这点其实也是源自于上面一点。上文说,往一个go函数中传递指针是不安全的。那么换个角度想,你怎么能保证你要调用的函数在函数实现内部不会使用go呢?如果不去看函数体内部具体实现,是没有办法确定的。

例如我们将上面的典型例子稍微改改

1
2
3
4
5
6
func main() {
     request := request.NewRequest()
     saveRequestToRedis1(request)
     saveRequestToRedis2(request)
     select {}
}

这下我们没有使用并发,就一定不会出现这问题了吧?追到函数里面去,傻眼了:

1
2
3
4
5
6
7
8
9
func saveReqeustToRedis1(request *Request) {
           
             go func() {
          
           request.ToUsers = []{1,2,3}
          ….
          redis.Save(request)
     }
}

我勒个去啊,里面起了一个goroutine,并修改了request指针指向的对象。这里就产生了错误了。好吧,如果在调用函数的时候,不看函数内部的具体实现,这个问题就无法避免。所以说呢?所以说,从最坏的思考角度出发,每个调用函数理论上来说都是不安全的!试想一下,这个调用函数如果不是自己开发组的人编写的,而是使用网络上的第三方开源代码...确实无法想象找出这个bug要花费多少时间。

3 goroutine的滥用陷阱

看一下这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
     go saveRequestToRedises(request)
}
 
func saveRequestToRedieses(request *Request) {
     for  _, redis := range Redises {
         go redis.saveRequestToRedis(request)
     }
}
 
func saveRequestToRedis(request *Request) {
             ….
             go func() {
                      request.ToUsers = []{1,2,3}
                        
                         redis.Save(request)
             }
 
}

神奇啊,go无处不在,好像眨眨眼就在哪里冒出来了。这就是go的滥用,到处都见到go,但是却不是很明确,哪里该用go?为什么用go?goroutine确实会有效率的提升么?

c语言的并发比go语言的并发复杂和繁琐地多,因此我们在使用之前会深思,考虑使用并发获得的好处和坏处。go呢?几乎不。

 

处理方法

下面说几个我处理这些问题的方法:

1 当启动一个goroutine的时候,如果一个函数必须要传递一个指针,但是函数层级很深,在无法保证安全的情况下,传递这个指针指向对象的一个克隆,而不是直接传递指针

1
2
3
4
5
6
7
8
fun main() {
     request := request.NewRequest()
     go saveRequestToRedis1(request.Clone())
     go saveReuqestToRedis2(request.Clone())
     
     select {}
 
}

Clone函数需要另外写。可以在结构体定义之后简单跟上这个方法。比如:

1
2
3
4
5
6
func ( this  *Request)Clone(){
     newRequest := NewRequst()
     newRequest.ToUsers = make([] int , len( this .ToUsers))
     copy(newRequest.ToUsers,  this .ToUsers)
 
}

其实从效率角度考虑这样确实会产生不必要的Clone的操作,耗费一定内存和CPU。但是在我看来,首先,为了安全性,这个尝试是值得的。其次,如果项目对效率确实有很高的要求,那么你不妨在开发阶段遵照这个原则使用clone,然后在项目优化阶段,作为一种优化手段,将不必要的Clone操作去掉。这样就能在保证安全的前提下做到最好的优化。

2 什么时候使用go的问题

有两种思维逻辑会想到使用goroutine:

1 业务逻辑需要并发

比如一个服务器,接收请求,阻塞式的方法是一个请求处理完成后,才开始第二个请求的处理。其实在设计的时候我们一定不会这么做,我们会在一开始就已经想到使用并发来处理这个场景,每个请求启动一个goroutine为它服务,这样就达到了并行的效果。这种goroutine直接按照思维的逻辑来使用goroutine

2 性能优化需要并发

一个场景是这样:需要给一批用户发送消息,正常逻辑会使用

1
2
3
4
for  _, user := range users {
     sendMessage(user)
 
}
1
  

但是在考虑到性能问题的时候,我们就不会这样做,如果users的个数很大,比如有1000万个用户?我们就没必要将1000万个用户放在一个routine中运行处理,考虑将1000万用户分成1000份,每份开一个goroutine,一个goroutine分发1万个用户,这样在效率上会提升很多。这种是性能优化上对goroutine的需求

 

按照项目开发的流程角度来看。在项目开发阶段,第一种思路的代码实现会直接影响到后续的开发实现,因此在项目开发阶段应该马上实现。但是第二种,项目中是由很多小角落是可以使用goroutine进行优化的,但是如果在开发阶段对每个优化策略都考虑到,那一定会直接打乱你的开发思路,会让你的开发周期延长,而且很容易埋下潜在的不安全代码。因此第二种情况在开发阶段绝不应该直接使用goroutine,而该在项目优化阶段以优化的思路对项目进行重构。

 

总结

总结下,文章写了这么多,并不是想让你对goroutine的使用产生畏惧,而是想强调一个观点:

goroutine的使用应该是保守型的。

在你敲下go这两个字母之前请仔细思考是否应该使用goroutine这柄利刃。

 

后续

在你看完这篇以后,也建议看看stevewang的这篇吧:

http://blog.sina.com.cn/s/blog_9be3b8f10101dsr6.html






本文转自轩脉刃博客园博客,原文链接:http://www.cnblogs.com/yjf512/archive/2012/06/30/2571247.html,如需转载请自行联系原作者

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6天前
|
监控 算法 Go
Golang深入浅出之-Go语言中的服务熔断、降级与限流策略
【5月更文挑战第4天】本文探讨了分布式系统中保障稳定性的重要策略:服务熔断、降级和限流。服务熔断通过快速失败和暂停故障服务调用来保护系统;服务降级在压力大时提供有限功能以保持整体可用性;限流控制访问频率,防止过载。文中列举了常见问题、解决方案,并提供了Go语言实现示例。合理应用这些策略能增强系统韧性和可用性。
30 0
|
1天前
|
Java Go
一文带你速通go语言指针
Go语言指针入门指南:简述指针用于提升效率,通过地址操作变量。文章作者sharkChili是Java/CSDN专家,维护Java Guide项目。文中介绍指针声明、取值,展示如何通过指针修改变量值及在函数中的应用。通过实例解析如何使用指针优化函数,以实现对原变量的直接修改。作者还邀请读者加入交流群深入探讨,并鼓励关注其公众号“写代码的SharkChili”。
8 0
|
1天前
|
存储 缓存 Java
来聊聊go语言的hashMap
本文介绍了Go语言中的`map`与Java的不同设计思想。作者`sharkChili`是一名Java和Go开发者,同时也是CSDN博客专家及JavaGuide项目的维护者。文章探讨了Go语言`map`的数据结构,包括`count`、`buckets指针`和`bmap`,解释了键值对的存储方式,如何利用内存对齐优化空间使用,并展示了`map`的初始化、插入键值对以及查找数据的源码过程。此外,作者还分享了如何通过汇编查看`map`操作,并鼓励读者深入研究Go的哈希冲突解决和源码。最后,作者提供了一个交流群,供读者讨论相关话题。
9 0
|
2天前
|
Java Go
Go语言学习11-数据初始化
【5月更文挑战第3天】本篇带大家通过内建函数 new 和 make 了解Go语言的数据初始化过程
15 1
Go语言学习11-数据初始化
|
2天前
|
自然语言处理 安全 Java
速通Go语言编译过程
Go语言编译过程详解:从词法分析(生成token)到句法分析(构建语法树),再到语义分析(类型检查、推断、匹配及函数内联)、生成中间码(SSA)和汇编码。最后,通过链接生成可执行文件。作者sharkchili,CSDN Java博客专家,分享技术细节,邀请读者加入交流群。
20 2
|
2天前
|
Java Linux Go
一文带你速通Go语言基础语法
本文是关于Go语言的入门介绍,作者因其简洁高效的特性对Go语言情有独钟。文章首先概述了Go语言的优势,包括快速上手、并发编程简单、设计简洁且功能强大,以及丰富的标准库。接着,文章通过示例展示了如何编写和运行Go代码,包括声明包、导入包和输出语句。此外,还介绍了Go的语法基础,如变量类型(数字、字符串、布尔和复数)、变量赋值、类型转换和默认值。文章还涉及条件分支(if和switch)和循环结构(for)。最后,简要提到了Go函数的定义和多返回值特性,以及一些常见的Go命令。作者计划在后续文章中进一步探讨Go语言的其他方面。
9 0
|
3天前
|
JavaScript 前端开发 Go
Go语言的入门学习
【4月更文挑战第7天】Go语言,通常称为Golang,是由Google设计并开发的一种编程语言,它于2009年公开发布。Go的设计团队主要包括Robert Griesemer、Rob Pike和Ken Thompson,这三位都是计算机科学和软件工程领域的杰出人物。
10 1
|
4天前
|
Go
|
4天前
|
分布式计算 Java Go
Golang深入浅出之-Go语言中的分布式计算框架Apache Beam
【5月更文挑战第6天】Apache Beam是一个统一的编程模型,适用于批处理和流处理,主要支持Java和Python,但也提供实验性的Go SDK。Go SDK的基本概念包括`PTransform`、`PCollection`和`Pipeline`。在使用中,需注意类型转换、窗口和触发器配置、资源管理和错误处理。尽管Go SDK文档有限,生态系统尚不成熟,且性能可能不高,但它仍为分布式计算提供了可移植的解决方案。通过理解和掌握Beam模型,开发者能编写高效的数据处理程序。
133 1
|
4天前
|
算法 关系型数据库 MySQL
Go语言中的分布式ID生成器设计与实现
【5月更文挑战第6天】本文探讨了Go语言在分布式系统中生成全局唯一ID的策略,包括Twitter的Snowflake算法、UUID和MySQL自增ID。Snowflake算法通过时间戳、节点ID和序列号生成ID,Go实现中需处理时间回拨问题。UUID保证全局唯一,但长度较长。MySQL自增ID依赖数据库,可能造成性能瓶颈。选择策略时需考虑业务需求和并发、时间同步等挑战,以确保系统稳定可靠。
111 0