你确定没有滥用 goroutine 吗

BytePioneer
• 阅读 1194

写在前面

学习 golang ,路还很长呢,犹记得刚开始学习 golang 的时候,写起来确实非常简单,有很多包和工具使用,不需要重复造轮子,但是要真的学好一门语言作为工具,对于其原理是非常有必要学懂的

并发错误

golang 天生高并发,编码的时候,就会出现滥用 goroutine 的情况,我们来看看都是如何滥用的

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(" the num is ", i)
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

xdm 看看上面这个简单的程序运行 go run main.go 输出会是什么呢?

是会输出 0 到 9 吗?,我们来实际看看效果

# go run main.go
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 the num is  10
 program over !!

哦豁,这是为啥,明明循环了 10 次,应该每一次递增 1 的打印出结果才对呀

你确定没有滥用 goroutine 吗

其实我们看到的这种现象属于 并发错误

解决错误

我们尝试着在 匿名函数中传入参数 i, 看看效果会不会好一点

func main() {
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println(" the num is ", i)
        }(i)
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

再次执行 go run main.go 查看输出

# go run main.go
 the num is  0
 the num is  1
 the num is  2
 the num is  3
 the num is  4
 the num is  5
 the num is  6
 the num is  7
 the num is  8
 the num is  9
 program over !!

果然,这才是我们想要的结果

那么回过头来细细看代码,我们可以发现,i 是主协程中的变量,主协程会修改 i 地址上的值, 变量 i 的地址一直在被重复使用,可是多个子协程也在不停的读取 i 的值,就导致了并发错误

避免这种并发错误,就可以用我们上面用到的传参拷贝即可

探究

我们再来确认一下,是不是 i 的地址一直是同一个

func main() {
    for i := 0; i < 10; i++ {
        fmt.Printf(" i = %d , &i = %p \n", i,&i)

        go func() {
            fmt.Printf(" the i  = %d , &i = %p \n", i,&i)
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

程序运行起来效果如下,主协程和子协程调用的 i 是同一个 i,地址完全相同

你确定没有滥用 goroutine 吗

我们再来看看解决并发错误的时候,i 的地址又是有何不同

func main() {
    for i := 0; i < 10; i++ {
        fmt.Printf(" i = %d , &i = %p \n", i,&i)

        go func(i int) {
            fmt.Printf(" the i  = %d , &i = %p \n", i,&i)
        }(i)
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

你确定没有滥用 goroutine 吗

我们可以看出,主协程中的 i 地址仍然是一样的,这个没错,但是子协程里面的 i 每一个协程的 i 变量地址都不一样,每个协程输出的都是属于自己的变量 i ,因此不会有上述的错误

程序崩溃 panic

有时候我们编码,会开辟多个协程,但是没有处理好协程中可能会 panic 的情况,若子协程挂掉,那么主协程也会随之挂掉,这里我们需要特别注意

举一个简单的例子

func main() {
    for i := 0; i < 5; i++ {

        go func() {
            a := 10
            b := 0
            fmt.Printf(" the i  = %d \n", a/b)
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

上面这个程序很明显,就是为了造 panic 的,是一个除 0 的异常,可想而知,整个程序会因为子协程的 panic 而挂掉

运行程序后报错信息如下:

# go run main.go
panic: runtime error: integer divide by zero

goroutine 5 [running]:
main.main.func1()
        /home/admin/golang_study/later_learning/goroutine_test/main.go:24 +0x11
created by main.main
        /home/admin/golang_study/later_learning/goroutine_test/main.go:21 +0x42
exit status 2

加入处理手段

我们在每一个子协程退出前都会去处理是否会有 panic,那么子协程的 panic 就不会导致 主协程挂掉了,这里谨记

func main() {
    for i := 0; i < 5; i++ {

        go func() {
            defer func() {
                if err := recover(); err != nil {
                    fmt.Println("recover one goroutine panic")
                }
            }()
            a := 10
            b := 0
            fmt.Printf(" the i  = %d \n", a/b)
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

程序运行效果如下:

# go run main.go
recover one goroutine panic
recover one goroutine panic
recover one goroutine panic
recover one goroutine panic
recover one goroutine panic
 program over !!

很明显程序是没有 panic 的,因为每一个子协程发生的 panic 都被处理掉了,我们还可以使用 golang 提供的 runtime 包来将 具体的 panic 信息打印出来,便于分析问题

来写一个简单的例子

func main() {
    for i := 0; i < 5; i++ {

        go func() {
            defer func() {
                if err := recover(); err != nil {
                    buf := make([]byte, 512)
                    last := runtime.Stack(buf, false)
                    fmt.Println("last == ",last)
                    buf = buf[:last]
                    fmt.Printf("[PANIC]%v\n%s\n", err, buf)
                }
            }()

            a := 10
            b := 0
            fmt.Printf(" the i  = %d \n", a/b)
        }()
    }
    time.Sleep(time.Second)
    fmt.Println(" program over !! ")
}

此处我们运用了 runtime.Stack(buf, false) 来计算goroutine panic 的堆栈信息的字节数,并最终打印出来

我们先来看效果

你确定没有滥用 goroutine 吗

我们将 panic 堆栈信息的字节数打印出来,并且将 panic 的具体信息也打印出来, 最重要的是程序没有崩溃

通过使用上述的方法就可以让子协程的 panic 不影响主协程的同时还可以打印出子协程 panic 的堆栈信息

可以看看源码

可以看看源码对于该函数的解释就明白了

你确定没有滥用 goroutine 吗

Stack 将调用 goroutine 的堆栈跟踪格式化为 buf,并返回写入buf的字节数。

如果为 true, Stack 将格式化所有其他 goroutine 的堆栈跟踪在当前 goroutine 的跟踪之后进入 buf。

golang 的技巧还很多,咱们需要用起来才能够体现它的价值

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

你确定没有滥用 goroutine 吗

好了,本次就到这里

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是阿兵云原生,欢迎点赞关注收藏,下次见~

点赞
收藏
评论区
推荐文章
学python的猫 学python的猫
4年前
零基础应该如何开始学习python
随着人工智能时代的到来,Python也在不断发展壮大,越来越多的人选择学Python,只要因为它容易学习,功能又强大还可以跨平台。其实Python作为一门脚本语言,难度上相较于其他语言略微简单点,但是对于没有计算机基础的人来说,也是非常难得,可能安装这一步就会难倒大家!1、Python学习确定方向对于刚入门的人来说,要先把Python基础和进阶学透,再继续往
Stella981 Stella981
3年前
Golang日志框架之logrus
golang日志库golang标准库的日志框架非常简单,仅仅提供了print,panic和fatal三个函数对于更精细的日志级别、日志文件分割以及日志分发等方面并没有提供支持。所以催生了很多第三方的日志库,但是在golang的世界里,没有一个日志库像slf4j那样在Java中具有绝对统治地位。golang中,流行的日志框架包括logr
Stella981 Stella981
3年前
Golang 内存管理源码剖析
Golang的内存管理基于tcmalloc,可以说起点挺高的。但是Golang在实现的时候还做了很多优化,我们下面通过源码来看一下Golang的内存管理实现。下面的源码分析基于go1.8rc3。1.tcmalloc介绍关于tcmalloc可以参考这篇文章 tcmalloc介绍(https://ww
Wesley13 Wesley13
3年前
Golang处理大数据时使用高效的Pipeline(流水线)执行模型
Golang被证明非常适合并发编程,goroutine比异步编程更易读、优雅、高效。本文提出一个适合由Golang实现的Pipeline执行模型,适合批量处理大量数据(ETL)的情景。想象这样的应用情景:(1)从数据库A(Cassandra)加载用户评论(量巨大,例如10亿条);(2)根据每条评论的用户ID、从数据库B(MySQL)关联用户资
Stella981 Stella981
3年前
Goroutine并发调度模型深入之实现一个协程池
并发(并行),一直以来都是一个编程语言里的核心主题之一,也是被开发者关注最多的话题;Go语言作为一个出道以来就自带『高并发』光环的富二代编程语言,它的并发(并行)编程肯定是值得开发者去探究的,而Go语言中的并发(并行)编程是经由goroutine实现的,goroutine是golang最重要的特性之一,具有使用成本低、消耗资源低、能效高等特点,官方宣称
Stella981 Stella981
3年前
Goroutine并发调度模型深度解析&手撸一个协程池
并发(并行),一直以来都是一个编程语言里的核心主题之一,也是被开发者关注最多的话题;Go语言作为一个出道以来就自带『高并发』光环的富二代编程语言,它的并发(并行)编程肯定是值得开发者去探究的,而Go语言中的并发(并行)编程是经由goroutine实现的,goroutine是golang最重要的特性之一,具有使用成本低、消耗资源低、能效高等特点,官方宣称
Wesley13 Wesley13
3年前
Go WEB入门
摘要由于Golang优秀的并发处理,很多公司使用Golang编写微服务。对于Golang来说,只需要短短几行代码就可以实现一个简单的Http服务器。加上Golang的协程,这个服务器可以拥有极高的性能。然而,正是因为代码过于简单,我们才应该去研究他的底层实现,做到会用,也知道为什么这么用。在本文中,会以自顶向下的方式,从如何使用,到如何实现,一点点的分
Stella981 Stella981
3年前
Go中的并发编程和goroutine
并发编程对于任何语言来说都不是一件简单的事情。Go在设计之初主打高并发,为使用者提供了goroutine,使用的方式虽然简单,但是用好却不是那么容易,我们一起来学习Go中的并发编程。1\.并行和并发并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。并发(concurrency):指在
Stella981 Stella981
3年前
Golang 果然是大杀器
已经将一个Service改成用Golang实现,效果非常好!这个Service的代码已经开源,GoTasks(http://git.oschina.net/janpoem/GoTasks)。Golang是我目前接触过的语言中,并发编程效率最高。node.js虽然也高,但是时间精准度上,就远远不如Golang了。一开始看了很多网上的教程,搞得乱七八
Stella981 Stella981
3年前
GoLang基础数据类型
GoLang基础数据类型字符串处理大全作者:尹正杰版权声明:原创作品,谢绝转载!否则将追究法律责任。欢迎加入:  高级运维工程师之路        598432640任何一门语言都有它的好处,Golang的长处就是在于它的高并发能力,所以现在练习好Golang的用法,有利于你找一份好的工作哟~在学习任何一本语言
Stella981 Stella981
3年前
Golang 中的并发限制与超时控制
前言上回在 用Go写一个轻量级的ssh批量操作工具(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fwww.jianshu.com%2Fp%2F7d315f8551ad) 里提及过,我们做Golang并发的时候要对并发进行限制,对goroutine的执行要有超
BytePioneer
BytePioneer
Lv1
载着我满满的怀念,你渐行渐远。
文章
4
粉丝
0
获赞
0