深刻理解Go-defer的原理剖析

Defer 也是Go裏面比較特別的一個關鍵字了,主要就是用來保證在程序執行過程當中,defer後面的函數都會被執行到,通常用來關閉鏈接、清理資源等。segmentfault

1. 結構概覽

1.1. defer

type _defer struct {
   siz     int32   // 參數的大小
   started bool    // 是否執行過了
   sp      uintptr // sp at time of defer
   pc      uintptr
   fn      *funcval 
   _panic  *_panic // defer中的panic
   link    *_defer // defer鏈表,函數執行流程中的defer,會經過 link這個 屬性進行串聯
}
複製代碼

1.2. panic

type _panic struct {
   argp      unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
   arg       interface{}    // argument to panic
   link      *_panic        // link to earlier panic
   recovered bool           // whether this panic is over
   aborted   bool           // the panic was aborted
}
複製代碼

1.3. g

由於 defer panic 都是綁定在 運行的g上的,因此這裏說明一下g中與 defer panic相關的屬性緩存

type g struct {
   _panic         *_panic // panic組成的鏈表
   _defer         *_defer // defer組成的先進後出的鏈表,同棧
}
複製代碼

2. 源碼分析

2.1. main

最開始,仍是經過go tool 來分析一下,底層是經過什麼函數來實現的吧bash

func main() {
	defer func() {
		recover()
	}()
	panic("error")
}
複製代碼

go build -gcflags=all="-N -l" main.goapp

go tool objdump -s "main.main" main函數

▶ go tool objdump -s "main\.main" main | grep CALL
  main.go:4             0x4548d0                e81b00fdff              CALL runtime.deferproc(SB)              
  main.go:7             0x4548f2                e8b90cfdff              CALL runtime.gopanic(SB)                
  main.go:4             0x4548fa                e88108fdff              CALL runtime.deferreturn(SB)            
  main.go:3             0x454909                e85282ffff              CALL runtime.morestack_noctxt(SB)       
  main.go:5             0x4549a6                e8d511fdff              CALL runtime.gorecover(SB)              
  main.go:4             0x4549b5                e8a681ffff              CALL runtime.morestack_noctxt(SB)
複製代碼

綜合反編譯結果能夠看出,defer 關鍵字首先會調用 runtime.deferproc 定義一個延遲調用對象,而後再函數結束前,調用 runtime.deferreturn 來完成 defer 定義的函數的調用源碼分析

panic 函數就會調用 runtime.gopanic 來實現相關的邏輯學習

recover 則調用 runtime.gorecover 來實現 recover 的功能ui

2.2. deferproc

根據 defer 關鍵字後面定義的函數 fn 以及 參數的size,來建立一個延遲執行的 函數,並將這個延遲函數,掛在到當前g的 _defer 的鏈表上this

func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
   sp := getcallersp()
   argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
   callerpc := getcallerpc()
   // 獲取一個_defer對象, 並放入g._defer鏈表的頭部
   d := newdefer(siz)
	 // 設置defer的fn pc sp等,後面調用
   d.fn = fn
   d.pc = callerpc
   d.sp = sp
   switch siz {
   case 0:
      // Do nothing.
   case sys.PtrSize:
      // _defer 後面的內存 存儲 argp的地址信息
      *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
   default:
      // 若是不是指針類型的參數,把參數拷貝到 _defer 的後面的內存空間
      memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
   }
   return0()
}
複製代碼

這個函數看起來比較簡答,經過newproc 獲取一個 _defer 的對象,並加入到當前g的 _defer 鏈表的頭部,而後再把參數或參數的指針拷貝到 獲取到的 _defer對象的 後面的內存空間atom

2.2.1. newdefer

newdefer 的做用是獲取一個*_defer*對象, 並推入 g._defer鏈表的頭部

func newdefer(siz int32) *_defer {
   var d *_defer
   // 根據 size 經過deferclass判斷應該分配的 sizeclass,就相似於 內存分配預先肯定好幾個sizeclass,而後根據size肯定sizeclass,找對應的緩存的內存塊
   sc := deferclass(uintptr(siz))
   gp := getg()
   // 若是sizeclass在既定的sizeclass範圍內,去g綁定的p上找
   if sc < uintptr(len(p{}.deferpool)) {
      pp := gp.m.p.ptr()
      if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
         // 當前sizeclass的緩存數量==0,且不爲nil,從sched上獲取一批緩存
         systemstack(func() {
            lock(&sched.deferlock)
            for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
               d := sched.deferpool[sc]
               sched.deferpool[sc] = d.link
               d.link = nil
               pp.deferpool[sc] = append(pp.deferpool[sc], d)
            }
            unlock(&sched.deferlock)
         })
      }
      // 若是從sched獲取以後,sizeclass對應的緩存不爲空,分配
      if n := len(pp.deferpool[sc]); n > 0 {
         d = pp.deferpool[sc][n-1]
         pp.deferpool[sc][n-1] = nil
         pp.deferpool[sc] = pp.deferpool[sc][:n-1]
      }
   }
   // p和sched都沒有找到 或者 沒有對應的sizeclass,直接分配
   if d == nil {
      // Allocate new defer+args.
      systemstack(func() {
         total := roundupsize(totaldefersize(uintptr(siz)))
         d = (*_defer)(mallocgc(total, deferType, true))
      })
   }
   d.siz = siz
   // 插入到g._defer的鏈表頭
   d.link = gp._defer
   gp._defer = d
   return d
}
複製代碼

根據size獲取sizeclass,對sizeclass進行分類緩存,這是內存分配時的思想

先去p上分配,而後批量從全局 sched上獲取到本地緩存,這種二級緩存的思想真的是遍及在go源碼的各個部分啊

2.3. deferreturn

func deferreturn(arg0 uintptr) {
   gp := getg()
   // 獲取g defer鏈表的第一個defer,也是最後一個聲明的defer
   d := gp._defer
   // 沒有defer,就不須要幹什麼事了
   if d == nil {
      return
   }
   sp := getcallersp()
   // 若是defer的sp與callersp不匹配,說明defer不對應,有多是調用了其餘棧幀的延遲函數
   if d.sp != sp {
      return
   }
   // 根據d.siz,把原先存儲的參數信息獲取並存儲到arg0裏面
   switch d.siz {
   case 0:
      // Do nothing.
   case sys.PtrSize:
      *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
   default:
      memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
   }
   fn := d.fn
   d.fn = nil
   // defer用過了就釋放了,
   gp._defer = d.link
   freedefer(d)
   // 跳轉到執行defer
   jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
}
複製代碼

2.3.1.freedefer

釋放defer用到的函數,應該跟調度器、內存分配的思想是同樣的

func freedefer(d *_defer) {
   // 判斷defer的sizeclass
   sc := deferclass(uintptr(d.siz))
   // 超出既定的sizeclass範圍的話,就是直接分配的內存,那就無論了
   if sc >= uintptr(len(p{}.deferpool)) {
      return
   }
   pp := getg().m.p.ptr()
   // p本地sizeclass對應的緩衝區滿了,批量轉移一半到全局sched
   if len(pp.deferpool[sc]) == cap(pp.deferpool[sc]) {
      // 使用g0來轉移
      systemstack(func() {
         var first, last *_defer
         for len(pp.deferpool[sc]) > cap(pp.deferpool[sc])/2 {
            n := len(pp.deferpool[sc])
            d := pp.deferpool[sc][n-1]
            pp.deferpool[sc][n-1] = nil
            pp.deferpool[sc] = pp.deferpool[sc][:n-1]
            // 先將須要轉移的那批defer對象串成一個鏈表
            if first == nil {
               first = d
            } else {
               last.link = d
            }
            last = d
         }
         lock(&sched.deferlock)
         // 把這個鏈表放到sched.deferpool對應sizeclass的鏈表頭
         last.link = sched.deferpool[sc]
         sched.deferpool[sc] = first
         unlock(&sched.deferlock)
      })
   }
   // 清空當前要釋放的defer的屬性
   d.siz = 0
   d.started = false
   d.sp = 0
   d.pc = 0
   d.link = nil

   pp.deferpool[sc] = append(pp.deferpool[sc], d)
}
複製代碼

二級緩存的思想,在 深刻理解Go-goroutine的實現及Scheduler分析深刻理解go-channel和select的原理深刻理解Go-垃圾回收機制 已經分析過了,就再也不過多分析了

2.4. gopanic

func gopanic(e interface{}) {
   gp := getg()

   var p _panic
   p.arg = e
   p.link = gp._panic
   gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))

   atomic.Xadd(&runningPanicDefers, 1)
   // 依次執行 g._defer鏈表的defer對象
   for {
      d := gp._defer
      if d == nil {
         break
      }

      // If defer was started by earlier panic or Goexit (and, since we're back here, that triggered a new panic),
      // take defer off list. The earlier panic or Goexit will not continue running.
      // 正常狀況下,defer執行完成以後都會被移除,既然這個defer沒有移除,緣由只有兩種: 1. 這個defer裏面引起了panic 2. 這個defer裏面引起了 runtime.Goexit,可是這個defer已經執行過了,須要移除,若是引起這個defer沒有被移除是第一個緣由,那麼這個panic也須要移除,由於這個panic也執行過了,這裏給panic增長標誌位,以待後續移除
      if d.started {
         if d._panic != nil {
            d._panic.aborted = true
         }
         d._panic = nil
         d.fn = nil
         gp._defer = d.link
         freedefer(d)
         continue
      }
      d.started = true

      // Record the panic that is running the defer.
      // If there is a new panic during the deferred call, that panic
      // will find d in the list and will mark d._panic (this panic) aborted.
      // 把當前的panic 綁定到這個defer上面,defer裏面有可能panic,這種狀況下就會進入到 上面d.started 的邏輯裏面,而後把當前的panic終止掉,由於已經執行過了 
      d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
      // 執行defer.fn
      p.argp = unsafe.Pointer(getargp(0))
      reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
      p.argp = nil

      // reflectcall did not panic. Remove d.
      if gp._defer != d {
         throw("bad defer entry in panic")
      }
      // 解決defer與panic的綁定關係,由於 defer函數已經執行完了,若是有panic或Goexit就不會執行到這裏了
      d._panic = nil
      d.fn = nil
      gp._defer = d.link

      // trigger shrinkage to test stack copy. See stack_test.go:TestStackPanic
      //GC()

      pc := d.pc
      sp := unsafe.Pointer(d.sp) // must be pointer so it gets adjusted during stack copy
      freedefer(d)
      // panic被recover了,就不須要繼續panic了,繼續執行剩餘的代碼
      if p.recovered {
         atomic.Xadd(&runningPanicDefers, -1)

         gp._panic = p.link
         // Aborted panics are marked but remain on the g.panic list.
         // Remove them from the list.
         // 從panic鏈表中移除aborted的panic,下面解釋
         for gp._panic != nil && gp._panic.aborted {
            gp._panic = gp._panic.link
         }
         if gp._panic == nil { // must be done with signal
            gp.sig = 0
         }
         // Pass information about recovering frame to recovery.
         gp.sigcode0 = uintptr(sp)
         gp.sigcode1 = pc
         // 調用recovery, 恢復當前g的調度執行
         mcall(recovery)
         throw("recovery failed") // mcall should not return
      }
   }
	 // 打印panic信息
   preprintpanics(gp._panic)
	 // panic
   fatalpanic(gp._panic) // should not return
   *(*int)(nil) = 0      // not reached
}
複製代碼

這裏解釋一下 gp._panic.aborted 的做用,如下面爲例

func main() {
   defer func() { // defer1
      recover()
   }()
   panic1()
}

func panic1() {
   defer func() {  // defer2
      panic("error1") // panic2
   }()
   panic("error")  // panic1
}
複製代碼
  1. 當執行到 panic("error")

    g._defer鏈表: g._defer->defer2->defer1

    g._panic鏈表:g._panic->panic1

  2. 當執行到 panic("error1")

    g._defer鏈表: g._defer->defer2->defer1

    g._panic鏈表:g._panic->panic2->panic1

  3. 繼續執行到 defer1 函數內部,進行recover()

    此時會去恢復 panic2 引發的 panic, panic2.recovered = true,應該順着g._panic鏈表繼續處理下一個panic了,可是咱們能夠發現 panic1 已經執行過了,這也就是下面的代碼的邏輯了,去掉已經執行過的panic

    for gp._panic != nil && gp._panic.aborted {
       gp._panic = gp._panic.link
    }
    複製代碼

panic的邏輯能夠梳理一下:

程序在遇到panic的時候,就再也不繼續執行下去了,先把當前panic 掛載到 g._panic 鏈表上,開始遍歷當前g的g._defer鏈表,而後執行_defer對象定義的函數等,若是 defer函數在調用過程當中又發生了 panic,則又執行到了 gopanic函數,最後,循環打印全部panic的信息,並退出當前g。然而,若是調用defer的過程當中,遇到了recover,則繼續進行調度(mcall(recovery))。

2.4.1. recovery

恢復一個被panic的g,從新進入並繼續執行調度

func recovery(gp *g) {
   // Info about defer passed in G struct.
   sp := gp.sigcode0
   pc := gp.sigcode1
   // Make the deferproc for this d return again,
   // this time returning 1. The calling function will
   // jump to the standard return epilogue.
   // 記錄defer返回的sp pc
   gp.sched.sp = sp
   gp.sched.pc = pc
   gp.sched.lr = 0
   gp.sched.ret = 1
   // 從新恢復執行調度
   gogo(&gp.sched)
}
複製代碼

2.5. gorecover

gorecovery 僅僅只是設置了 g._panic.recovered 的標誌位

func gorecover(argp uintptr) interface{} {
   gp := getg()
   p := gp._panic
   // 須要根據 argp的地址,判斷是否在defer函數中被調用
   if p != nil && !p.recovered && argp == uintptr(p.argp) {
      // 設置標誌位,上面gopanic中會對這個標誌位作判斷
      p.recovered = true
      return p.arg
   }
   return nil
}
複製代碼

2.6. goexit

咱們還忽略了一個點,當咱們手動調用 runtime.Goexit() 退出的時候,defer函數也會執行,咱們分析一下這種狀況

func Goexit() {
	// Run all deferred functions for the current goroutine.
	// This code is similar to gopanic, see that implementation
	// for detailed comments.
	gp := getg()
  // 遍歷defer鏈表
	for {
		d := gp._defer
		if d == nil {
			break
		}
    // 若是 defer已經執行過了,與defer綁定的panic 終止掉
		if d.started {
			if d._panic != nil {
				d._panic.aborted = true
				d._panic = nil
			}
			d.fn = nil
      // 從defer鏈表中移除
			gp._defer = d.link
      // 釋放defer
			freedefer(d)
			continue
		}
    // 調用defer內部函數
		d.started = true
		reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz))
		if gp._defer != d {
			throw("bad defer entry in Goexit")
		}
		d._panic = nil
		d.fn = nil
		gp._defer = d.link
		freedefer(d)
		// Note: we ignore recovers here because Goexit isn't a panic
	}
  // 調用goexit0,清除當前g的屬性,從新進入調度
	goexit1()
}
複製代碼

2.7. 圖示解析

源碼這一塊閱讀起來難度並非很大,若是還有什麼疑惑,但願下面的一副動圖能解開你的疑惑

做圖做的略拙劣,見諒

步驟解析:

  1. L3: 生成一個defer1,放到g._defer鏈表上
  2. L11: 生成一個defer2,掛載到g._defer鏈表上
  3. L14: panic1 調用 gopanic,將當前panic放到g._panic鏈表上
  4. L14: 由於panic1,從g._defer 鏈表頭部提取到defer2,開始執行
  5. L12: 執行defer2,又一個panic,掛載到g._panic鏈表上
  6. L12: 由於panic2,從g._defer鏈表頭部提取到defer2,發現defer2已經執行過了移出鏈表,,且defer2是由於panic1而觸發的,跳過defer2,並abort panic1
  7. L12: 繼續提取g._defer鏈表的下一個,提取到defer1
  8. L5: defer1 執行recover,recover掉panic2,移除鏈表,判斷下一個panic,即panic1,panic1已經被defer2 aborted掉了,移除panic1
  9. defer1 執行完了,移除defer1

3. 關聯文檔

4. 參考文檔

  • 《Go語言學習筆記》--雨痕
相關文章
相關標籤/搜索