#Golang# Golang与并发编程(3) goroutine泄漏

#Golang# Golang与并发编程(3) goroutine泄漏

介绍 goroutine 泄漏的相关内容,主要讨论以下三个部分

  • goroutine 泄漏判断
  • goroutine 泄漏分类
  • goroutine 泄漏预防

目录 Table of Contents


前情提要

goroutine 泄漏指的是因为编码中的陷阱使得 goroutine 无法正常释放而造成的内存泄漏,甚至导致内存溢出或最终程序崩溃。

本文将讨论 goroutine泄漏的判断、分类和预防,如有错漏,欢迎指出 ;P

goroutine泄漏判断

runtime.NumGoroutine

  • runtime.NumGoroutine 可以返回正在运行中的 goroutine 数量(包括 main goroutinenormal goroutine
1
2
3
4
5
6
7
8
9
10
import(
"fmt"
"runtime"
)

// ...

fmt.Println(runtime.NumGoroutine())

// ...

runtime/pprof

  • runtime/pprof 可以(以写入的形式)返回 goroutine运行数量和堆栈信息
1
2
3
4
5
6
7
8
9
10
import (  
"os"
"runtime/pprof"
)

// ...

pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)

// ...

http/net/pprof

  • http/net/pprof 可以(以访问的形式)返回 goroutine运行数量、堆栈信息和其它资源信息
1
2
3
4
5
6
7
8
9
10
11
12
import (
"net/http"
_ "net/http/pprof"
)

// ...

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

// ...

// 进入 http://localhost:6060/debug/pprof/goroutine?debug=1 查看

gops

  • gops 支持列出当前环境下的进程信息

    • .go 中:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      import (
      "log"
      "github.com/google/gops/agent"
      )

      // ...

      if err := agent.Start(); err != nil {
      log.Fatalln(err)
      }

      // ...
    • terminal 中:

      1
      2
      3
      4
      5
      $ gops # 查看进程信息

      $ gops stats PID # 查看状态信息

      $ gops stack PID # 查看堆栈信息

leaktest

  • leaktest 将泄漏检测过程加入到自动化测试中去。
1
2
3
4
5
6
7
8
9
10
11
import (
"github.com/fortytw2/leaktest"
)

// ...

defer leaktest.Check(t)
defer leaktest.CheckTimeout(t, time.Second)
defer leaktest.CheckContext(ctx, t)

// ...

goroutine 泄漏分类

无退出的计算循环

  • 对于没有函数调用,纯循环计算的 Gruntime 无法实行抢占
  • 显然,如果没有退出机制且程序常驻的话,每次启动的 goroutine 都得不到释放,就会发生 goroutine 泄漏。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"runtime"
"time"
)

func test() {
for {
fmt.Println("Testing...") // 死循环无法抢占和回收
}
}

func main() {
defer func() {
time.Sleep(time.Second)
fmt.Println("NumGoroutine:", runtime.NumGoroutine()) // 输出为 2 ,发生泄漏
}()

go test()
}

不结束的I/O请求

  • 对于 I/O 请求runtime 无法实行抢占
  • 如果 I/O 请求一直处于等待期间,该 goroutine 则无法释放,出现泄漏。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"bufio"
"fmt"
"os"
"runtime"
"time"
)

func test() {
input := bufio.NewScanner(os.Stdin)
if input.Scan() {
text := input.Text() // I/O请求无法抢占和回收
fmt.Println(text)
}
}

func main() {
defer func() {
time.Sleep(time.Second)
fmt.Println("NumGoroutine:", runtime.NumGoroutine()) // 输出为 2 ,发生泄漏
}()

go test()
}

channel 引起的泄漏

只发送不接收

  • 上游生产速度远远大于下游消费速度,阻塞的 goroutine 会一直在 channel 的发送等待队列
  • 无缓冲 channel 没有接收就会阻塞,有缓冲 channel 缓冲满了就会阻塞。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
"math/rand"
"runtime"
"time"
)

func query() int {
n := rand.Intn(100)
return n
}

func queryAll() int {
ch := make(chan int)
go func() { ch <- query() }()
go func() { ch <- query() }()
go func() { ch <- query() }()
return <-ch
}

func main() {
defer func() {
time.Sleep(time.Second)
fmt.Println("NumGoroutine:", runtime.NumGoroutine()) // 输出为 3 ,发生泄漏
}()

queryAll()
}

只接收不发送

  • 下游消费速度远远大于上游生产速度,阻塞的 goroutine 会一直在 channel 的接收等待队列
  • 无缓冲 channel 仍然接收就会阻塞,有缓冲 channel 缓冲空了就会阻塞。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"fmt"
"runtime"
"time"
)

func main() {
defer func() {
time.Sleep(time.Second)
fmt.Println("NumGoroutine:", runtime.NumGoroutine()) // 输出为 2 ,发生泄漏
}()

ch := make(chan bool)
go func() {
ch <- true
}()
}

空 channel

  • nil channel 发送和接收数据都会导致阻塞,只进行声明而不初始化 channel 容易出现该类泄漏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"runtime"
"time"
)

func test() {
var ch chan bool
<-ch
// ch <- data
}

func main() {
defer func() {
time.Sleep(time.Second)
fmt.Println("NumGoroutine:", runtime.NumGoroutine()) // 输出为 2 ,发生泄漏
}()

go test()
}

空 select

  • select{} 永远无法响应导致协程阻塞,一般不会出现这种情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"runtime"
"time"
)

func test() {
select {}
}

func main() {
defer func() {
time.Sleep(time.Second)
fmt.Println("NumGoroutine:", runtime.NumGoroutine()) // 输出为 2 ,发生泄漏
}()

go test()
}

sync 引起的泄漏

Mutex 忘记解锁

  • 有一个 goroutine 加锁忘了解锁,另一个 goroutine 竞争锁会失败,由此这个 goroutine 将一直地阻塞。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"runtime"
"sync"
"time"
)

func test() {
var mutex sync.Mutex

for i := 1; i <= 2; i++ {
go func() {
mutex.Lock()

fmt.Println("goroutine index:", i)
}()
}
}

func main() {
defer func() {
time.Sleep(time.Second)
fmt.Println("NumGoroutine:", runtime.NumGoroutine()) // 输出为 2 ,发生泄漏
}()

go test()
}

WaitGroup 计数错误

  • WaiteGroupAddDone 数量不对应将引起 Wait 的等待退出条件永远无法满足,从而阻塞协程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"runtime"
"sync"
"time"
)

func test() {
var wg sync.WaitGroup

wg.Add(2)

go func() {
wg.Done()
}()

wg.Wait()
}

func main() {
defer func() {
time.Sleep(time.Second)
fmt.Println("NumGoroutine:", runtime.NumGoroutine()) // 输出为 2 ,发生泄漏
}()

go test()
}

Cond 没发信号

  • Wait 方法阻塞等待条件变量满足条件,如果没有 Signal 或者 BroadcastWait 将会一直不能唤醒。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
"fmt"
"runtime"
"sync"
"time"
)

func test() {
cond := sync.NewCond(&sync.Mutex{})

condition := false

go func() {
cond.L.Lock()

condition = true

cond.L.Unlock()
}()

cond.L.Lock()
for !condition {
cond.Wait()
}
cond.L.Unlock()
}

func main() {
defer func() {
time.Sleep(time.Second)
fmt.Println("NumGoroutine:", runtime.NumGoroutine()) // 输出为 2 ,发生泄漏
}()

go test()
}

goroutine泄漏预防

  • 对于计算循环和 I/O 请求:检查代码

    • 确保每个计算循环都会退出;
    • 确保每个 I/O 请求都会关闭。
  • 对于 channel 引起的泄漏:本质上是防止 channel 阻塞

    • 使用有缓冲的 channel
    • make 来声明并初始化 channel
    • 避免空的 select
    • 优雅地关闭 channel 等。
  • 对于 sync 引起的泄漏:本质上是防止 sync 阻塞

    • 对于互斥锁加锁解锁应该成对地出现,这时候可以利用 defer

      1
      2
      3
      4
      5
      // ...
      mutex.Lock()
      // ...
      defer mutex.Unlock()
      // ...
    • 对于信号量Add(1)Done() 搭配使用,而不是一开始就规定好任务计数:

      1
      2
      3
      4
      5
      6
      // ...
      wg.Add(1)
      go func() {
      wg.Done()
      // ...
      }()
    • 对于条件变量条件改变后应发送信号,单播还是多播根据具体情况而定:

      1
      2
      3
      4
      // ...
      condition = true
      cond.Signal() // or cond.Broadcast()
      // ...

参考链接


Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×