关于golang:给大家丢脸了用了三年golang我还是没答对这道respBodyClose-引发的内存泄漏题

52次阅读

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

给大家争脸了,用了三年 golang,我还是没答对这道内存透露题。

问题

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "runtime"
)

func main() {
    num := 6
    for index := 0; index < num; index++ {resp, _ := http.Get("https://www.baidu.com")
        _, _ = ioutil.ReadAll(resp.Body)
    }
    fmt.Printf("此时 goroutine 个数 = %d\n", runtime.NumGoroutine())
}

下面这道题在不执行 resp.Body.Close() 的状况下,透露了吗?如果透露,透露了多少个goroutine?

怎么答

  • 不进行 resp.Body.Close(),透露是肯定的。然而透露的goroutine 个数就让我迷糊了。因为执行了 6 遍,每次透露一个 读和写 goroutine,就是 12 个 goroutine,加上main 函数 自身也是一个goroutine,所以答案是13.
  • 然而执行程序,发现 答案是 3 ,出入有点大,为什么呢?

解释

  • 咱们间接看源码。golanghttp 包。
http.Get()

-- DefaultClient.Get
----func (c *Client) do(req *Request)
------func send(ireq *Request, rt RoundTripper, deadline time.Time)
-------- resp, didTimeout, err = send(req, c.transport(), deadline) 
// 以上代码在 go/1.12.7/libexec/src/net/http/client:174 

func (c *Client) transport() RoundTripper {
    if c.Transport != nil {return c.Transport}
    return DefaultTransport
}
  • 阐明 http.Get 默认应用 DefaultTransport 治理连贯。
DefaultTransport 是干嘛的呢?
// It establishes network connections as needed
// and caches them for reuse by subsequent calls.
  • DefaultTransport 的作用是依据须要建设网络连接并缓存它们以供后续调用重用。
那么 DefaultTransport 什么时候会建设连贯呢?

接着下面的代码堆栈往下翻

func send(ireq *Request, rt RoundTripper, deadline time.Time) 
--resp, err = rt.RoundTrip(req) // 以上代码在 go/1.12.7/libexec/src/net/http/client:250
func (t *Transport) RoundTrip(req *http.Request)
func (t *Transport) roundTrip(req *Request)
func (t *Transport) getConn(treq *transportRequest, cm connectMethod)
func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (*persistConn, error) {
    ...
    go pconn.readLoop()  // 启动一个读 goroutine
    go pconn.writeLoop() // 启动一个写 goroutine
    return pconn, nil
}
  • 一次建设连贯,就会启动一个 读 goroutine 写 goroutine。这就是为什么一次 http.Get() 会透露 两个 goroutine的起源。
  • 透露的起源晓得了,也晓得是因为没有执行close
那为什么不执行 close 会透露呢?
  • 回到刚刚启动的 读 goroutinereadLoop() 代码里
func (pc *persistConn) readLoop() {
    alive := true
    for alive {
        ...
        // Before looping back to the top of this function and peeking on
        // the bufio.Reader, wait for the caller goroutine to finish
        // reading the response body. (or for cancelation or death)
        select {
        case bodyEOF := <-waitForBodyRead:
            pc.t.setReqCanceler(rc.req, nil) // before pc might return to idle pool
            alive = alive &&
                bodyEOF &&
                !pc.sawEOF &&
                pc.wroteRequest() &&
                tryPutIdleConn(trace)
            if bodyEOF {eofc <- struct{}{}}
        case <-rc.req.Cancel:
            alive = false
            pc.t.CancelRequest(rc.req)
        case <-rc.req.Context().Done():
            alive = false
            pc.t.cancelRequest(rc.req, rc.req.Context().Err())
        case <-pc.closech:
            alive = false
        }
        ...
    }
}
  • 简略来说 readLoop 就是一个死循环,只有 alivetruegoroutine就会始终存在
  • select 里是 goroutine 有可能 退出的场景:

    • body 被读取结束或 body 敞开
    • request 被动 cancel
    • requestcontext Done 状态 true
    • 以后的 persistConn 敞开

其中第一个 body 被读取完或敞开这个 case:

alive = alive &&
    bodyEOF &&
    !pc.sawEOF &&
    pc.wroteRequest() &&
    tryPutIdleConn(trace)

bodyEOF 来源于到一个通道 waitForBodyRead,这个字段的 truefalse 间接决定了 alive 变量的值(alive=true 读 goroutine持续活着,循环,否则退出goroutine)。

那么这个通道的值是从哪里过去的呢?
// go/1.12.7/libexec/src/net/http/transport.go: 1758
        body := &bodyEOFSignal{
            body: resp.Body,
            earlyCloseFn: func() error {
                waitForBodyRead <- false
                <-eofc // will be closed by deferred call at the end of the function
                return nil

            },
            fn: func(err error) error {
                isEOF := err == io.EOF
                waitForBodyRead <- isEOF
                if isEOF {<-eofc // see comment above eofc declaration} else if err != nil {if cerr := pc.canceled(); cerr != nil {return cerr}
                }
                return err
            },
        }
  • 如果执行 earlyCloseFnwaitForBodyRead 通道输出的是 falsealive 也会是 false,那 readLoop() 这个 goroutine 就会退出。
  • 如果执行 fn,其中包含失常状况下 body 读完数据抛出 io.EOF 时的 casewaitForBodyRead 通道输出的是 true,那 alive 会是 true,那么 readLoop() 这个 goroutine 就不会退出,同时还顺便执行了 tryPutIdleConn(trace)
// tryPutIdleConn adds pconn to the list of idle persistent connections awaiting
// a new request.
// If pconn is no longer needed or not in a good state, tryPutIdleConn returns
// an error explaining why it wasn't registered.
// tryPutIdleConn does not close pconn. Use putOrCloseIdleConn instead for that.
func (t *Transport) tryPutIdleConn(pconn *persistConn) error
  • tryPutIdleConnpconn 增加到期待新申请的闲暇长久连贯列表中,也就是之前说的连贯会复用。
那么问题又来了,什么时候会执行这个 fnearlyCloseFn 呢?
func (es *bodyEOFSignal) Close() error {es.mu.Lock()
    defer es.mu.Unlock()
    if es.closed {return nil}
    es.closed = true
    if es.earlyCloseFn != nil && es.rerr != io.EOF {return es.earlyCloseFn() // 敞开时执行 earlyCloseFn
    }
    err := es.body.Close()
    return es.condfn(err)
}
  • 下面这个其实就是咱们比拟收悉的 resp.Body.Close() , 在外面会执行 earlyCloseFn,也就是此时 readLoop() 里的 waitForBodyRead 通道输出的是 falsealive 也会是 false,那 readLoop() 这个 goroutine 就会退出,goroutine 不会泄露。
b, err = ioutil.ReadAll(resp.Body)
--func ReadAll(r io.Reader) 
----func readAll(r io.Reader, capacity int64) 
------func (b *Buffer) ReadFrom(r io.Reader)


// go/1.12.7/libexec/src/bytes/buffer.go:207
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
    for {
        ...
        m, e := r.Read(b.buf[i:cap(b.buf)])  // 看这里,是 body 在执行 read 办法
        ...
    }
}
  • 这个read,其实就是 bodyEOFSignal 里的
func (es *bodyEOFSignal) Read(p []byte) (n int, err error) {
    ...
    n, err = es.body.Read(p)
    if err != nil {
        ... 
    // 这里会有一个 io.EOF 的报错,意思是读完了
        err = es.condfn(err)
    }
    return
}


func (es *bodyEOFSignal) condfn(err error) error {
    if es.fn == nil {return err}
    err = es.fn(err)  // 这了执行了 fn
    es.fn = nil
    return err
}
  • 下面这个其实就是咱们比拟收悉的读取 body 里的内容。ioutil.ReadAll() , 在读完 body 的内容时会执行 fn,也就是此时 readLoop() 里的 waitForBodyRead 通道输出的是 truealive 也会是 true,那 readLoop() 这个 goroutine 就不会退出,goroutine 会泄露,而后执行 tryPutIdleConn(trace) 把连贯放回池子里复用。

总结

  • 所以论断跃然纸上了,尽管执行了 6 次循环,而且每次都没有执行 Body.Close() , 就是因为执行了 ioutil.ReadAll() 把内容都读出来了,连贯得以复用,因而只透露了一个 读 goroutine和一个 写 goroutine,最初加上main goroutine,所以答案就是 3 个 goroutine
  • 从另外一个角度说,失常状况下咱们的代码都会执行 ioutil.ReadAll(),但如果此时忘了 resp.Body.Close(),的确会导致透露。但如果你 调用的域名始终是同一个 的话,那么只会透露一个 读 goroutine 和一个 写 goroutine这就是为什么代码明明不标准但却看不到显著内存透露的起因
  • 那么问题又来了,为什么下面要特意强调是同一个域名呢?改天,回头,当前有空再说吧。

文章举荐:

  • 连 nil 切片和空切片一不一样都不分明?那 BAT 面试官只好让你回去等告诉了。
  • 昨天那个在 for 循环里 append 元素的共事,明天还在么?
  • golang 面试官:for select 时,如果通道曾经敞开会怎么样?如果只有一个 case 呢?
  • golang 面试官:for select 时,如果通道曾经敞开会怎么样?如果只有一个 case 呢?
  • golang 面试题:对曾经敞开的的 chan 进行读写,会怎么样?为什么?
  • golang 面试题:对未初始化的的 chan 进行读写,会怎么样?为什么?
  • golang 面试题:reflect(反射包)如何获取字段 tag?为什么 json 包不能导出公有变量的 tag?
  • golang 面试题:json 包变量不加 tag 会怎么样?
  • golang 面试题:怎么防止内存逃逸?
  • golang 面试题:简略聊聊内存逃逸?
  • golang 面试题:字符串转成 byte 数组,会产生内存拷贝吗?
  • golang 面试题:翻转含有中文、数字、英文字母的字符串
  • golang 面试题:拷贝大切片肯定比小切片代价大吗?
  • golang 面试题:能说说 uintptr 和 unsafe.Pointer 的区别吗?
如果你想每天学习一个知识点?

正文完
 0