关于go:你确定没有滥用-goroutine-吗

51次阅读

共计 3177 个字符,预计需要花费 8 分钟才能阅读完成。

写在后面

学习 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 的打印出后果才对呀

其实咱们看到的这种景象属于 并发谬误

解决谬误

咱们尝试着在 匿名函数中传入参数 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,地址完全相同

咱们再来看看解决并发谬误的时候,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 !!")
}

咱们能够看出,主协程中的 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 的堆栈信息的字节数,并最终打印进去

咱们先来看成果

咱们将 panic 堆栈信息的字节数打印进去,并且将 panic 的具体信息也打印进去,最重要的是程序没有解体

通过应用上述的办法就能够让子协程的 panic 不影响主协程的同时还能够打印出子协程 panic 的堆栈信息

能够看看源码

能够看看源码对于该函数的解释就明确了

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

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

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

欢送点赞,关注,珍藏

敌人们,你的反对和激励,是我保持分享,提高质量的能源

好了,本次就到这里

技术是凋谢的,咱们的心态,更应是凋谢的。拥抱变动,背阴而生,致力向前行。

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

正文完
 0