关于golang:跟面试官聊-Goroutine-泄露的-6-种方法真刺激

3次阅读

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

微信搜寻【脑子进煎鱼了】关注这一只爆肝煎鱼。本文 GitHub github.com/eddycjy/blog 已收录,有我的系列文章、材料和开源 Go 图书。

大家好,我是煎鱼。

前几天分享 Go 群友发问的文章时,有读者在朋友圈下提到,心愿我可能针对 Goroutine 泄露这块进行解说,他在面试的时候常常被问到。

明天的男主角,就是 Go 语言的著名品牌标识 Goroutine,一个随随便便就能开几十万个慢车进车道的大杀器。

    for {go func() {}()
    }

本文会聚焦于 Goroutine 泄露的 N 种办法,进行详解和阐明。

为什么要问

面试官为啥会问 Goroutine(协程)泄露这种奇异的问题呢?

能够猜想是:

  • Goroutine 切实是应用门槛切实是太低了,顺手就一个就能起,呈现了不少滥用的状况。例如:并发 map。
  • Goroutine 自身在 Go 语言的规范库、复合类型、底层源码中利用宽泛。例如:HTTP Server 对每一个申请的解决就是一个协程去运行。

很多 Go 工程在线上出事变时,根本 Goroutine 的关联,大家都会作为救火队长,风风火火的跑去看指标、看日志,通过 PProf 采集 Goroutine 运行状况等。

天然他也就是最受注目的那颗“星”了,所以在日常面试中,被问几率也就极高了。

Goroutine 泄露

理解分明大家爱问的起因后,咱们开始对 Goroutine 泄露的 N 种办法进行钻研,心愿通过前人留下的“坑”,理解其原理和避开这些问题。

泄露的起因大多集中在:

  • Goroutine 内正在进行 channel/mutex 等读写操作,但因为逻辑问题,某些状况下会被始终阻塞。
  • Goroutine 内的业务逻辑进入死循环,资源始终无奈开释。
  • Goroutine 内的业务逻辑进入长时间期待,有一直新增的 Goroutine 进入期待。

接下来我会援用在网上冲浪收集到的一些 Goroutine 泄露例子(会在文末参考注明出处)。

channel 使用不当

Goroutine+Channel 是最经典的组合,因而不少泄露都呈现于此。

最经典的就是下面提到的 channel 进行读写操作时的逻辑问题。

发送不接管

第一个例子:

func main() {
    for i := 0; i < 4; i++ {queryAll()
        fmt.Printf("goroutines: %d\n", runtime.NumGoroutine())
    }
}

func queryAll() int {ch := make(chan int)
    for i := 0; i < 3; i++ {go func() {ch <- query() }()}
    return <-ch
}

func query() int {n := rand.Intn(100)
    time.Sleep(time.Duration(n) * time.Millisecond)
    return n
}

输入后果:

goroutines: 3
goroutines: 5
goroutines: 7
goroutines: 9

在这个例子中,咱们调用了屡次 queryAll 办法,并在 for 循环中利用 Goroutine 调用了 query 办法。其重点在于调用 query 办法后的后果会写入 ch 变量中,接管胜利后再返回 ch 变量。

最初可看到输入的 goroutines 数量是在一直减少的,每次多 2 个。也就是每调用一次,都会泄露 Goroutine。

起因在于 channel 均曾经发送了(每次发送 3 个),然而在接收端并没有接管齐全(只返回 1 个 ch),所诱发的 Goroutine 泄露。

接管不发送

第二个例子:

func main() {defer func() {fmt.Println("goroutines:", runtime.NumGoroutine())
    }()

    var ch chan struct{}
    go func() {ch <- struct{}{}}()
    
    time.Sleep(time.Second)
}

输入后果:

goroutines:  2

在这个例子中,与“发送不接管”两者是绝对的,channel 接管了值,然而不发送的话,同样会造成阻塞。

但在理论业务场景中,个别更简单。根本是一大堆业务逻辑里,有一个 channel 的读写操作呈现了问题,天然就阻塞了。

nil channel

第三个例子:

func main() {defer func() {fmt.Println("goroutines:", runtime.NumGoroutine())
    }()

    var ch chan int
    go func() {<-ch}()
    
    time.Sleep(time.Second)
}

输入后果:

goroutines:  2

在这个例子中,能够得悉 channel 如果遗记初始化,那么无论你是读,还是写操作,都会造成阻塞。

失常的初始化姿态是:

    ch := make(chan int)
    go func() {<-ch}()
    ch <- 0
    time.Sleep(time.Second)

调用 make 函数进行初始化。

奇怪的慢期待

第四个例子:

func main() {
    for {go func() {_, err := http.Get("https://www.xxx.com/")
            if err != nil {fmt.Printf("http.Get err: %v\n", err)
            }
            // do something...
    }()

    time.Sleep(time.Second * 1)
    fmt.Println("goroutines:", runtime.NumGoroutine())
    }
}

输入后果:

goroutines:  5
goroutines:  9
goroutines:  13
goroutines:  17
goroutines:  21
goroutines:  25
...

在这个例子中,展现了一个 Go 语言中经典的事变场景。也就是个别咱们会在应用程序中去调用第三方服务的接口。

然而第三方接口,有时候会很慢,久久不返回响应后果。恰好,Go 语言中默认的 http.Client 是没有设置超时工夫的。

因而就会导致始终阻塞,始终阻塞就一爽快,Goroutine 天然也就继续暴涨,一直泄露,最终占满资源,导致事变。

在 Go 工程中,咱们个别倡议至多对 http.Client 设置超时工夫:

    httpClient := http.Client{Timeout: time.Second * 15,}

并且要做限流、熔断等措施,以防突发流量造成依赖崩塌,仍然吃 P0。

互斥锁遗记解锁

第五个例子:

func main() {
    total := 0
    defer func() {time.Sleep(time.Second)
        fmt.Println("total:", total)
        fmt.Println("goroutines:", runtime.NumGoroutine())
    }()

    var mutex sync.Mutex
    for i := 0; i < 10; i++ {go func() {mutex.Lock()
            total += 1
        }()}
}

输入后果:

total:  1
goroutines:  10

在这个例子中,第一个互斥锁 sync.Mutex 加锁了,然而他可能在解决业务逻辑,又或是遗记 Unlock 了。

因而导致前面的所有 sync.Mutex 想加锁,却因未开释又都阻塞住了。个别在 Go 工程中,咱们倡议如下写法:

    var mutex sync.Mutex
    for i := 0; i < 10; i++ {go func() {mutex.Lock()
            defer mutex.Unlock()
            total += 1
    }()}

同步锁使用不当

第六个例子:

func handle(v int) {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < v; i++ {fmt.Println("脑子进煎鱼了")
        wg.Done()}
    wg.Wait()}

func main() {defer func() {fmt.Println("goroutines:", runtime.NumGoroutine())
    }()

    go handle(3)
    time.Sleep(time.Second)
}

在这个例子中,咱们调用了同步编排 sync.WaitGroup,模仿了一遍咱们会从内部传入循环遍历的控制变量。

但因为 wg.Add 的数量与 wg.Done 数量并不匹配,因而在调用 wg.Wait 办法后始终阻塞期待。

在 Go 工程中应用,咱们会倡议如下写法:

    var wg sync.WaitGroup
    for i := 0; i < v; i++ {wg.Add(1)
        defer wg.Done()
        fmt.Println("脑子进煎鱼了")
    }
    wg.Wait()

排查办法

咱们能够调用 runtime.NumGoroutine 办法来获取 Goroutine 的运行数量,进行前后一比拟,就能晓得有没有泄露了。

但在业务服务的运行场景中,Goroutine 内导致的泄露,大多数处于生产、测试环境,因而更多的是应用 PProf:

import (
    "net/http"
     _ "net/http/pprof"
)

http.ListenAndServe("localhost:6060", nil))

只有咱们调用 http://localhost:6060/debug/pprof/goroutine?debug=1,PProf 会返回所有带有堆栈跟踪的 Goroutine 列表。

也能够利用 PProf 的其余个性进行综合查看和剖析,这块参考我之前写的《Go 大杀器之性能分析 PProf》,根本是全村最全的教程了。

总结

在明天这篇文章中,咱们针对 Goroutine 泄露的 N 种常见的形式办法进行了一一剖析,虽说看起来都是比拟根底的场景。

但联合在理论业务代码中,就是一大坨中的某个细节导致全盘皆输了,心愿下面几个案例可能给大家带来警觉。

而面试官爱问,怕不是本人踩过许多坑,也心愿进来的同僚,也是南征北战了。

靠谱的工程师,而非只是八股工程师。

若有任何疑难欢送评论区反馈和交换,最好的关系是相互成就 ,各位的 点赞 就是煎鱼创作的最大能源,感激反对。

文章继续更新,能够微信搜【脑子进煎鱼了】浏览,回复【000】有我筹备的一线大厂面试算法题解和材料;本文 GitHub github.com/eddycjy/blog 已收录,欢送 Star 催更。

正文完
 0