我們先來看一下 Rob Pike 在 Google I/O 2012 的 talk “Go Concurrency Patterns” 中,有一段 snippet 會造成 momery leak。

先解釋一下這段 code,First 吃兩個 parameters,很明顯的 query 是個字串, replicas 則是一個 slice of Search,那 Search 是什麼?這邊底下有看到 replicas[i](query),得知他是一個 func 而且還接受一個 string parameter。

那他為什麼會 momery leak?

我們看到有一個 for range loop 在 iterate replicas,for each replica 都建立一個 goroutine 讓他去執行 search 的工作,當她完成了他會把結果塞進一個 type 是 Result 的 channel,在 func First 的結尾有一個 <-c 在 block,直到有一個 goroutine 把 Result 塞進 channel,所以先回來的 goroutine 就會是 func First 的 return value,但是後回來的就會是一個 deadlock 沒有一個 channel 等待接收他的結果,因為 func First 已經結束了。

func First(query string, replicas ...Search) Result {  
    c := make(chan Result)
    searchReplica := func(i int) { c <- replicas[i](query) }
    for i := range replicas {
        go searchReplica(i)
    }
    return <-c
}

我們要怎麼知道他真的 momery leak 了?

我們要用 gdb!

在用 gdb 之前,我們先要有一個完整可執行的範例程式

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

type search func(query string) result

type result struct {
	provider string
	status   int
}

func main() {
	replicas := []search{google, yahoo}
	r := first("lovebugs", replicas...)
	fmt.Println(r)
	time.Sleep(2 * time.Second)
	fmt.Println("woke up")
}

func google(query string) result {
	res, err := http.Get("http://google.com?q=" + query)
	if err != nil {
		log.Fatal(err)
	}

	return result{
		provider: "google",
		status:   res.StatusCode,
	}
}

func yahoo(query string) result {
	res, err := http.Get("http://yahoo.com/search?p=" + query)
	if err != nil {
		log.Fatal(err)
	}
	return result{
		provider: "yahoo",
		status:   res.StatusCode,
	}
}

func first(query string, replicas ...search) result {
	c := make(chan result)
	searchReplica := func(i int) {
		c <- replicas[i](query)
	}
	for i := range replicas {
		go searchReplica(i)
	}

	return <-c
}

編譯他 go build -gcflags "-N -l" main.go

gdb 執行他 gdb main

看到下面這個畫面就是我們開始執行了

GNU gdb (GDB) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin15.5.0".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from main...done.
Loading Go Runtime support.
(gdb)

先把我們要的 breakpoint 下好

(gdb) b 51
Breakpoint 1 at 0x29a5: file /Users/nukr/workspace/gocode/src/github.com/nukr/golang_test/blocked_goroutines_and_resource_lea
ks/leaks/main.go, line 51.
(gdb) b 22
Breakpoint 2 at 0x221b: file /Users/nukr/workspace/gocode/src/github.com/nukr/golang_test/blocked_goroutines_and_resource_lea
ks/leaks/main.go, line 22.
(gdb)

第一個我們要看的是 goroutine 在執行的時候是在哪個位置,這樣才能知道在 func main 結尾的時候,他的執行狀況。

我們下 run

(gdb) run
Starting program: /Users/nukr/workspace/gocode/src/github.com/nukr/golang_test/blocked_goroutines_and_resource_leaks/leaks/ma
in
[New Thread 0x126f of process 9680]
[New Thread 0x1403 of process 9680]
[New Thread 0x1503 of process 9680]
[New Thread 0x1603 of process 9680]

Thread 1 hit Breakpoint 1, main.first.func1 (i=1)
    at /Users/nukr/workspace/gocode/src/github.com/nukr/golang_test/blocked_goroutines_and_resource_leaks/leaks/main.go:51
51                      c <- replicas[i](query)

info goroutine 看一下

(gdb) info goroutines
  1 waiting  runtime.gopark
* 17 syscall  runtime.goexit
  2 waiting  runtime.gopark
  3 waiting  runtime.gopark
  18 waiting  runtime.gopark
* 19 running  main.first.func1
* 20 running  main.first.func1

我們看到 19, 20 這兩個就是我們的兩個 goroutine

再下 c

(gdb) c
Continuing.
[Switching to Thread 0x1403 of process 9680]

Thread 3 hit Breakpoint 1, main.first.func1 (i=0)
    at /Users/nukr/workspace/gocode/src/github.com/nukr/golang_test/blocked_goroutines_and_resource_leaks/leaks/main.go:51
51                      c <- replicas[i](query)

info goroutine 看現在 goroutine 的狀況

再下 cfmt.Println("woke up")info goroutine 確認最後的 goroutine 狀況

(gdb) info goroutines
* 1 running  runtime.gopark
* 17 syscall  runtime.goexit
  2 waiting  runtime.gopark
  3 waiting  runtime.gopark
  18 waiting  runtime.gopark
  20 waiting  runtime.gopark
  41 waiting  runtime.gopark
* 4 syscall  runtime.systemstack_switch
  38 waiting  runtime.gopark
  33 waiting  runtime.gopark
  50 waiting  runtime.gopark
  37 waiting  runtime.gopark
  51 waiting  runtime.gopark
  39 waiting  runtime.gopark
  36 waiting  runtime.gopark
  10 waiting  runtime.gopark
  31 waiting  runtime.gopark
  42 waiting  runtime.gopark
  32 waiting  runtime.gopark
  66 waiting  runtime.gopark

我們看到我們等了兩秒了,但是 20 的 goroutine 還在 memory leak comfirm!