热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

极简示例golang并发三坑

极简示例golang并发三坑-我们经常会遇到一些耗时的任务,然后又需要拿到任务处理后的结果作进一步处理,在go语言中首先想到的莫过于goroutine加等待组wg的方式来并发处理加

我们经常会遇到一些耗时的任务,然后又需要拿到任务处理后的结果作进一步处理, 在go语言中首先想到的莫过于goroutine加等待组wg的方式来并发处理加快效率。尽管在go中写并发程序已经足够简单了,但对一部分人来说往往一不注意就会掉进坑里。本文通过一个简单的例子梳理几个踩坑点。

简单的例子

业务场景中,我们往往需要将多个任务或者一个任务拆分,然后分别作复杂的逻辑处理。假如我们有1到10个数字代表了这10个任务,然后需要分别对这10个数字乘以2,以此代表逻辑处理。

坑点一

猜猜下面的程序会输出什么?

var (
    wg   = sync.WaitGroup{}
    nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
)

func task(num int) int {
    return num * 2
}

func main() {
    wg.Add(len(nums))
    for _, num := range nums {
        go func() {
            defer wg.Done()
            res := task(num)
            fmt.Println(res)
        }()
    }
    wg.Wait()
}

如果你的结论是"以不确定的顺序输出2,4,6,8,10,12,14,16,18,20",那么恭喜你入坑了!运行代码的实际结果为:

20
20
20
20
20
20
20
20
20
20

因为for循环中的goroutine在实际运行的时候,循环已经执行完毕了,num的值为循环后的最后一个值20。解决这个问题也很简单,在很多语言中也是如此,通过闭包的方式让每一个go func()独自保存其num值。

这种最基本的坑,虽然可以运行,但编辑器往往会有提示的

修正代码如下,便可以不确定的顺序输出2,4,6,8,10,12,14,16,18,20:

var (
    wg   = sync.WaitGroup{}
    nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
)

func task(num int) int {
    return num * 2
}

func main() {
    wg.Add(len(nums))
    for _, num := range nums {
        go func(num int) {
            defer wg.Done()
            res := task(num)
            fmt.Println(res)
        }(num)
    }
    wg.Wait()
}

坑点二

在上面的例子中,有些有着严格内存管理要求的小伙伴,可能会不假思索的改成这样:

var (
    wg   = sync.WaitGroup{}
    nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
)

func task(num int) int {
    return num * 2
}

func main() {
    wg.Add(len(nums))
    for _, num := range nums {
        go func(num *int) {
            defer wg.Done()
            res := task(*num)
            fmt.Println(res)
        }(&num)
    }
    wg.Wait()
}

这又会输出什么结果呢?不一定,但大部分值都为20:

20
20
20
20
20
8
20
20
20
20

同样是闭包,为什么传指针就不行了呢?恰恰是因为闭包,go func()里保存了同一个内存地址,即&numfor循环中指向的是同一个内存地址,但该地址上存储的值在for中不断发生变化,goroutine实际执行时,&num上的值基本都已经变成了最后一个值20.所以这时候不能传递指针。

可能有些人会说,是不是傻,一个简单的int类型,没事去传个指针干啥?这都能入坑。在实际情况中,可能确实没有人会这么做,怪就怪在本文的例子太过简单,如若这里的num不是int类型呢?现实写代码的时候,这里往往可能是一个复杂的结构体,比如orm中定义的model,我想肯定会有人传递&model的!

坑点三

紧接着上面的例子,假如我们想将处理后的结果保存起来,很自然的写出了如下代码:

var (
    wg   = sync.WaitGroup{}
    nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    results  []int
)

func task(num int) int {
    return num * 2
}

func main() {
    wg.Add(len(nums))
    for _, num := range nums {
        go func(num int) {
            defer wg.Done()
            res := task(num)
            results = append(results, res)
        }(num)
    }
    wg.Wait()
    fmt.Println(results)
}

results又会输出什么结果呢?如果你的结论是results中包含顺序不定的2,4,6,8,10,12,14,16,18,20,那么恭喜你又入坑了!正常情况下,len(results)的值应该为10,但上面代码多运行几次的结果表明,len(results)的值几乎都是小于10的。因为在go中,切片slice类型是非并发安全的,也就是说results中的某一个位置在同一时刻插入了多个值,最终造成了数据丢失。解决的办法可以通过加锁的方式:

var (
    wg   = sync.WaitGroup{}
    nums = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    results  []int
    lock  = sync.RWMutex{}
)

func task(num int) int {
    return num * 2
}

func main() {
    wg.Add(len(nums))
    for _, num := range nums {
        go func(num int) {
            defer wg.Done()
            res := task(num)
            lock.Lock()
            results = append(results, res)
            lock.Unlock()
        }(num)
    }
    wg.Wait()
    fmt.Println(results)
}

类似slice类型,go中map类型也是非并发安全的,在并发场景中我们可以使用sync.Map代替。

小结

就像那单细胞生物,越是简单反而越让人头疼!上面三个踩坑点其实都非常简单,大部分的人可能都是跟我一样的心态:"这么简单的问题我是不会入坑的!"。然而,自认为go代码我已经写得很熟练了,却不曾想最近在业务代码中被疯狂打脸,浪费很多时间!可能就关注代码本身而言,我想很少有人会掉进坑里。道理都懂,但现实中当我们的思路总是关注于复杂的业务逻辑如何组织代码实现时,一长串一长串的代码往往会让我们疏于这些细节。谨以此文为诫。


推荐阅读
  • Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication)。类型T表示任意的一种类型双向:chan ... [详细]
  • 认真一点学 Go:18. 并发
    收录于《Go基础系列》,作者:潇洒哥老苗。>>原文链接学到什么并发与并行的区别?什么是Goroutine?什么是通道?Goroutine如何通信?相关函数的使用?sel ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • 按照之前我对map的理解,map中的数据应该是有序二叉树的存储顺序,正常的遍历也应该是有序的遍历和输出,但实际试了一下,却发现并非如此,网上查了下,发现从Go1开始,遍历的起始节点就是随机了,当然随机 ... [详细]
  • golang 解析磁力链为 torrent 相关的信息
    其实通过http请求已经获得了种子的信息了,但是传播存储种子好像是违法的,所以就存储些描述信息吧。之前python跑的太慢了。这个go并发不知道写的有没有问题?!packag ... [详细]
  • Go 快速入门指南命令行参数
    命令行参数个数调用os包即可。获取参数个数,遍历参数packagemainimport(fmtos)funcmain(){fmt.Printf(Numberofargsi ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 本文讨论了如何使用IF函数从基于有限输入列表的有限输出列表中获取输出,并提出了是否有更快/更有效的执行代码的方法。作者希望了解是否有办法缩短代码,并从自我开发的角度来看是否有更好的方法。提供的代码可以按原样工作,但作者想知道是否有更好的方法来执行这样的任务。 ... [详细]
  • 欢乐的票圈重构之旅——RecyclerView的头尾布局增加
    项目重构的Git地址:https:github.comrazerdpFriendCircletreemain-dev项目同步更新的文集:http:www.jianshu.comno ... [详细]
  • 本文介绍了贝叶斯垃圾邮件分类的机器学习代码,代码来源于https://www.cnblogs.com/huangyc/p/10327209.html,并对代码进行了简介。朴素贝叶斯分类器训练函数包括求p(Ci)和基于词汇表的p(w|Ci)。 ... [详细]
  • 千万不要错过的后端[纯干货]面试知识点整理 I I
    千万不要错过的后端【纯干货】面试知识点整理IIc++内存管理上次分享整理的面试知识点I,今天我们来继续分享面试知识点整理IIlinuxkernel内核空间、内存管理、进程管理设备、 ... [详细]
  • funcReadXlsx(c[]CmdbTest,SheetNamestring)error{打开文件,如果文件不存在创建,存在就打开path:.cm ... [详细]
  • Go冒泡排序练习
    package main要求:随机生成5个元素的数组,并使用冒泡排序对其排序  从小到大思路分析:随机数用mathrand生成为了更好 ... [详细]
  • [Redis 系列]redis 学习六,redis 事务处理和监控事务
    【Redis系列】redis学习六,redis事务处理和监控事务写在前面我们学过的事务都是保证原子性的,但是redis的事务中执行多个指令,是不保证原子性的redis事务的本质就是 ... [详细]
  • 小编给大家分享一下Golang端口复用测试的实现方法,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有 ... [详细]
author-avatar
love糸_603
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有