<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go中被閉包捕獲的變量何時(shí)會(huì)被回收

          共 12080字,需瀏覽 25分鐘

           ·

          2021-09-22 23:45

          1. Go函數(shù)閉包

          Go語言原生提供了對(duì)閉包(closure)的支持。在Go語言中,閉包就是函數(shù)字面值[2]。Go規(guī)范中是這樣詮釋閉包的:

          函數(shù)字面值(function literals)是閉包:它們可以引用其包裹函數(shù)(surrounding function)中定義的變量。然后,這些變量在包裹函數(shù)和函數(shù)字面值之間共享,只要它們可以被訪問,它們就會(huì)繼續(xù)存在。

          閉包在Go語言中有著廣泛的應(yīng)用,最常見的就是與go關(guān)鍵字一起聯(lián)合使用創(chuàng)建一個(gè)新goroutine,比如下面標(biāo)準(zhǔn)庫中net/http包中的一段代碼:

          // $GOROOT/src/net/http/fileTransport.go

          00 func (t fileTransport) RoundTrip(req *Request) (resp *Response, err error) {
          01     rw, resc := newPopulateResponseWriter()
          02     go func() {
          03         t.fh.ServeHTTP(rw, req)
          04         rw.finish()
          05     }()
          06     return <-resc, nil
          07 }

          上面這段代碼中的RoundTrip方法就是使用go關(guān)鍵字結(jié)合閉包創(chuàng)建了一個(gè)新的goroutine,并且在這個(gè)goroutine中運(yùn)行的函數(shù)還引用了本屬于其外部包裹函數(shù)的變量:t、rw和req,或者說兩者共享這些變量。

          原本僅在RoundTrip方法內(nèi)部使用的變量一旦被“共享”給了其他函數(shù),那么它就無法在棧上分配了,逃逸到堆上是確定性事件。

          那么問題來了!這些被引用或叫被閉包捕獲的分配在堆上的外部變量何時(shí)能被回收呢?也許上面的例子還十分容易理解,當(dāng)新創(chuàng)建的goroutine執(zhí)行完畢后,這些變量就可以回收了。那么下面的閉包函數(shù)呢?

          func foo() func(int) int {
              i := []int{0: 10, 1: 11, 15: 128}
              return func(n int) int {
                  n+=i[0]
                  return n 
              }
          }

          在這個(gè)foo函數(shù)中,被閉包函數(shù)捕獲的長度為16的切片變量i何時(shí)可以被回收呢?

          注:我們定義閉包時(shí),喜歡用引用外部包裹函數(shù)的變量這種說法,但在Go編譯器的實(shí)現(xiàn)代碼[3]中,使用的是capture var,翻譯過來就是“被捕獲的變量”,所以這里也用了“捕獲”一詞來表示那些被閉包共享使用的外部包裹函數(shù)甚至是更外層函數(shù)中的變量。

          foo函數(shù)的返回值類型是一個(gè)函數(shù),也就是說foo函數(shù)的本地變量i被foo返回的新創(chuàng)建的閉包函數(shù)所捕獲,i不會(huì)被回收。通常一個(gè)堆上的內(nèi)存對(duì)象有明確的引用它的對(duì)象或指向它的地址的指針,該對(duì)象才會(huì)繼續(xù)存活,當(dāng)其不可達(dá)(unreachable)時(shí),即再?zèng)]有引用它的對(duì)象或指向它的指針時(shí)才會(huì)被GC回收。

          那么,變量i究竟是被誰引用了呢?變量i將在何時(shí)被回收呢?

          我們先回頭看一個(gè)非閉包的一般函數(shù):

          func f1() []int {
              i := []int{0: 10, 1: 11, 15: 128}
           return i     
          }

          func f2() {
           sl := f1()
              sl[0] = sl[0] + 10
              fmt.Println(sl)
          }

          func main() {
              f2()
          }

          我們看到f1將自己的局部切片變量i返回后,該變量被f2函數(shù)中的sl所引用,f2函數(shù)執(zhí)行完成后,切片變量i將變成unreachable,GC將回收該變量對(duì)應(yīng)的堆內(nèi)存。

          如果換成閉包函數(shù),比如前面的foo函數(shù),我們很大可能是這么來用的:

          // https://github.com/bigwhite/experiments/tree/master/closure/closure1.go

           1 package main
           2 
           3 import "fmt"
           4 
           5 func foo() func(int) int {
           6     i := []int{0: 10, 1: 11, 15: 128}
           7     return func(n int) int {
           8         n += i[0]
           9         return n
          10     }
          11 }
          12 
          13 func bar() {
          14     f := foo()
          15     a := f(5)
          16     fmt.Println(a)
          17 }
          18 
          19 func main() {
          20     bar()
          21     g := foo()
          22     b := g(6)
          23     fmt.Println(b)
          24 }

          在這里例子中,只要閉包函數(shù)中引用了foo函數(shù)的本地變量。這突然讓我想起了“在Go中,函數(shù)也是一等公民的特性[4]”。難道是閉包函數(shù)這一對(duì)象引用了foo函數(shù)的本地變量? 那么閉包函數(shù)在內(nèi)存布局上是如何引用到foo函數(shù)的本地整型切片變量i的呢?閉包函數(shù)在內(nèi)存布局中被映射為什么了呢?

          如果一門編程語言對(duì)某種語言元素的創(chuàng)建和使用沒有限制,我們可以像對(duì)待值(value)一樣對(duì)待這種語法元素,那么我們就稱這種語法元素是這門編程語言的“一等公民”。

          2. Go閉包函數(shù)對(duì)象

          要解答這個(gè)問題,我們只能尋求Go匯編[5]的幫助。我們生成上面的closure1.go的匯編代碼(我們使用go 1.16.5版本Go編譯器):

          $go tool compile -S closure1.go > closure1.s

          在匯編代碼中,我們找到closure1.go中第7行創(chuàng)建一個(gè)閉包函數(shù)所對(duì)應(yīng)的匯編代碼:

          // https://github.com/bigwhite/experiments/tree/master/closure/closure1.s

              0x0052 00082 (closure1.go:7)    LEAQ    type.noalg.struct { F uintptr; "".i []int }(SB), CX
              0x0059 00089 (closure1.go:7)    MOVQ    CX, (SP)
              0x005d 00093 (closure1.go:7)    PCDATA  $1$1
              0x005d 00093 (closure1.go:7)    NOP
              0x0060 00096 (closure1.go:7)    CALL    runtime.newobject(SB)
              0x0065 00101 (closure1.go:7)    MOVQ    8(SP), AX
              0x006a 00106 (closure1.go:7)    LEAQ    "".foo.func1(SB), CX
              0x0071 00113 (closure1.go:7)    MOVQ    CX, (AX)
              0x0074 00116 (closure1.go:7)    MOVQ    $16, 16(AX)
              0x007c 00124 (closure1.go:7)    MOVQ    $16, 24(AX)
              0x0084 00132 (closure1.go:7)    PCDATA  $0, $-2
              0x0084 00132 (closure1.go:7)    CMPL    runtime.writeBarrier(SB), $0
              0x008b 00139 (closure1.go:7)    JNE 165
              0x008d 00141 (closure1.go:7)    MOVQ    ""..autotmp_7+16(SP), CX
              0x0092 00146 (closure1.go:7)    MOVQ    CX, 8(AX)
              0x0096 00150 (closure1.go:7)    PCDATA  $0, $-1
              0x0096 00150 (closure1.go:7)    MOVQ    AX, "".~r0+40(SP)
              0x009b 00155 (closure1.go:7)    MOVQ    24(SP), BP
              0x00a0 00160 (closure1.go:7)    ADDQ    $32, SP
              0x00a4 00164 (closure1.go:7)    RET
              0x00a5 00165 (closure1.go:7)    PCDATA  $0, $-2
              0x00a5 00165 (closure1.go:7)    LEAQ    8(AX), DI
              0x00a9 00169 (closure1.go:7)    MOVQ    ""..autotmp_7+16(SP), CX
              0x00ae 00174 (closure1.go:7)    CALL    runtime.gcWriteBarrierCX(SB)
              0x00b3 00179 (closure1.go:7)    JMP 150
              0x00b5 00181 (closure1.go:7)    NOP

          匯編總是晦澀難懂。我們重點(diǎn)看第一行:

              0x0052 00082 (closure1.go:7)    LEAQ    type.noalg.struct { F uintptr; "".i []int }(SB), CX

          我們看到對(duì)應(yīng)到Go源碼中創(chuàng)建閉包函數(shù)的第7行,這行匯編代碼大致意思是將一個(gè)結(jié)構(gòu)體對(duì)象的地址放入CX。我們把這個(gè)結(jié)構(gòu)體對(duì)象摘錄出來:

          struct {
              F uintptr
              i []int
          }

          這個(gè)結(jié)構(gòu)體對(duì)象是哪里來的呢?顯然是Go編譯器根據(jù)閉包函數(shù)的“特征”創(chuàng)建出來的。其中的F就是閉包函數(shù)自身的地址,畢竟是函數(shù),這個(gè)地址與一般函數(shù)的地址應(yīng)該是在一個(gè)內(nèi)存區(qū)域(比如rodata的只讀數(shù)據(jù)區(qū)),那么整型切片變量i呢?難道這就是閉包函數(shù)所捕獲的那個(gè)Foo函數(shù)本地變量i。沒錯(cuò)!正是它。如果不信,我們可以再定義一個(gè)捕獲更多變量的閉包函數(shù)來驗(yàn)證一下。

          下面是一個(gè)捕獲3個(gè)整型變量的閉包函數(shù)的生成函數(shù):

          // https://github.com/bigwhite/experiments/tree/master/closure/closure2.go

          func foo() func(int) int {
              var a, b, c int = 11, 12, 13
              return func(n int) int {
                  a += n
                  b += n
                  c += n
                  return a + b + c
              }
          }

          其對(duì)應(yīng)的匯編代碼中那個(gè)閉包函數(shù)結(jié)構(gòu)為:

          0x0084 00132 (closure2.go:10)   LEAQ    type.noalg.struct { F uintptr; "".a *int; "".b *int; "".c *int }(SB), CX

          將該結(jié)構(gòu)體提取出來,即:

          struct {
           F uintptr
           a *int
           b *int
           c *int
          }

          到這里,我們證實(shí)了引用了包裹函數(shù)本地變量的正是閉包函數(shù)自身,即編譯器為其在內(nèi)存中建立的閉包函數(shù)結(jié)構(gòu)體對(duì)象。通過unsafe包,我們甚至可以輸出這個(gè)閉包函數(shù)對(duì)象。以closure2.go為例,我們來嘗試一下,如下面代碼所示。

          // https://github.com/bigwhite/experiments/tree/master/closure/closure2.go

          func foo() func(int) int {
              var a, b, c int = 11, 12, 13
              return func(n int) int {
                  a += n
                  b += n
                  c += n
                  return a + b + c
              }
          }

          type closure struct {
              f uintptr
              a *int
              b *int
              c *int
          }

          func bar() {
              f := foo()
              f(5)
              pc := *(**closure)(unsafe.Pointer(&f))
              fmt.Printf("%#v\n", *pc)
              fmt.Printf("a=%d, b=%d,c=%d\n", *pc.a, *pc.b, *pc.c)
              f(6)
              fmt.Printf("a=%d, b=%d,c=%d\n", *pc.a, *pc.b, *pc.c)
          }

          在上面代碼中,我們參考匯編的輸出定義了closure這個(gè)結(jié)構(gòu)體來對(duì)應(yīng)內(nèi)存中的閉包函數(shù)對(duì)象(每種閉包對(duì)象都是不同的,一個(gè)技巧就是參考匯編輸出的對(duì)象來定義),通過unsafe的地址轉(zhuǎn)換,我們將內(nèi)存中的閉包對(duì)象映射到closure結(jié)構(gòu)體實(shí)例上。運(yùn)行上面程序,我們可以得到如下輸出:

          $go run closure2.go
          main.closure{f:0x10a4d80, a:(*int)(0xc000118000), b:(*int)(0xc000118008), c:(*int)(0xc000118010)}
          a=16, b=17,c=18
          a=22, b=23,c=24

          在上面的例子中,閉包函數(shù)捕獲了外部變量a、b和c,這些變量實(shí)質(zhì)上被編譯器創(chuàng)建的閉包內(nèi)存對(duì)象所引用。當(dāng)我們調(diào)用foo函數(shù)時(shí),閉包函數(shù)對(duì)象創(chuàng)建(其地址賦值給變量f)。這樣,f對(duì)象一直引用著變量a、b和c。只有當(dāng)f被回收,a、b和c才會(huì)因unreachable而被回收。

          如果我們?cè)陂]包函數(shù)中僅僅是對(duì)捕獲的外部變量進(jìn)行只讀操作,那么閉包函數(shù)對(duì)象不會(huì)存儲(chǔ)這些變量的指針,而僅會(huì)做一份值拷貝。當(dāng)然,如果某個(gè)變量被一個(gè)函數(shù)中創(chuàng)建的多個(gè)閉包所捕獲,并且有的只讀,有的修改,那么閉包函數(shù)對(duì)象還是會(huì)存儲(chǔ)該變量的地址的。

          了解了閉包函數(shù)的本質(zhì),我們?cè)賮砜幢疚臉?biāo)題中的問題就容易多了。其答案就是在捕捉變量的閉包函數(shù)對(duì)象被回收后,如果這些被捕捉的變量沒有其他引用,它們將變?yōu)閡nreachable的,后續(xù)就會(huì)被GC回收了

          3. 小結(jié)

          我們回顧一下文章開頭引用的Go語言規(guī)范中對(duì)閉包詮釋中提到的一句話:“只要它們可以被訪問,它們就會(huì)繼續(xù)存在”。現(xiàn)在看來,我們可以將其理解為:只要閉包函數(shù)對(duì)象存在,其捕獲的那些變量就會(huì)存在,就不會(huì)被回收

          閉包函數(shù)的這種機(jī)制決定了我們?cè)谌粘J褂眠^程中也要時(shí)刻考慮著閉包函數(shù)所捕獲的變量可能的“延遲回收”。如果某個(gè)場景下,閉包引用的變量占用內(nèi)存較大,且閉包函數(shù)對(duì)象被創(chuàng)建出的數(shù)量很多且因業(yè)務(wù)需要延遲很久才會(huì)被執(zhí)行(比如定時(shí)器場景),這就會(huì)導(dǎo)致堆內(nèi)存可能長期處于高水位,我們要考慮內(nèi)存容量是否能承受這樣的水位,如果不能,則要考慮更換實(shí)現(xiàn)方案了。

          本文涉及的所有代碼可以從這里下載[6]:https://github.com/bigwhite/experiments/tree/master/closure

          4. 參考資料

          • 深入理解函數(shù)閉包 - https://zhuanlan.zhihu.com/p/56750616
          • Go語言高級(jí)編程 - https://github.com/chai2010/advanced-go-programming-book/blob/master/ch3-asm/ch3-06-func-again.md#366-閉包函數(shù)

          參考資料

          [1] 

          本文永久鏈接: https://tonybai.com/2021/08/09/when-variables-captured-by-closures-are-recycled-in-go

          [2] 

          函數(shù)字面值: https://tip.golang.org/ref/spec#Function_literals

          [3] 

          Go編譯器的實(shí)現(xiàn)代碼: https://github.com/golang/go/tree/master/src/cmd/compile/internal/gc/closure.go

          [4] 

          在Go中,函數(shù)也是一等公民的特性: https://www.imooc.com/read/87/article/2420

          [5] 

          Go匯編: https://tip.golang.org/doc/asm

          [6] 

          這里下載: https://github.com/bigwhite/experiments/tree/master/closure

          [7] 

          改善Go語?編程質(zhì)量的50個(gè)有效實(shí)踐: https://www.imooc.com/read/87

          [8] 

          Kubernetes實(shí)戰(zhàn):高可用集群搭建、配置、運(yùn)維與應(yīng)用: https://coding.imooc.com/class/284.html

          [9] 

          我愛發(fā)短信: https://51smspush.com/

          [10] 

          鏈接地址: https://m.do.co/c/bff6eed92687



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 60
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品无码7777777 | 亚洲综合婷婷五月 | 青青草亚洲 | 亚洲欧洲日本无在线码 | 欧美操逼视蘋 |