Skip to content

defer

defer trong phát triển go hàng ngày là một từ khóa xuất hiện với tần suất rất cao, nó sẽ thực thi hàm liên kết với defer theo cách vào trước ra sau, trong nhiều trường hợp chúng ta tận dụng cơ chế này để thực hiện các thao tác giải phóng tài nguyên, như đóng file.

go
fd, err := os.Open("/dev/stdin")
if err != nil{
    return err
}
defer fd.Close()
...

Từ khóa xuất hiện với tần suất cao như vậy khiến chúng ta cần phải hiểu rõ cấu trúc đằng sau nó.

Cấu trúc

Từ khóa defer tương ứng với cấu trúc runtime._defer của runtime, cấu trúc của nó không phức tạp

go
type _defer struct {
  started bool
  heap    bool
  openDefer bool
  sp        uintptr // sp at time of defer
  pc        uintptr // pc at time of defer
  fn        func()  // can be nil for open-coded defers
  _panic    *_panic // panic that is running defer
  link      *_defer // next defer on G; can point to either heap or stack!
  fd   unsafe.Pointer // funcdata for the function associated with the frame
  varp uintptr        // value of varp for the stack frame
  framepc uintptr
}

Trong đó trường fn là hàm tương ứng với từ khóa defer, link biểu thị defer liên kết tiếp theo, sppc ghi lại thông tin hàm của bên gọi, dùng để phán đoán defer thuộc về hàm nào. defer tồn tại dưới hình thức danh sách liên kết trong runtime, đầu của danh sách nằm trên goroutine G, nên defer thực tế liên kết trực tiếp với goroutine.

go
type g struct {
    ...
  _panic    *_panic // innermost panic - offset known to liblink
  _defer    *_defer // innermost defer
    ...
}

Khi goroutine thực thi hàm, sẽ theo thứ tự thêm defer trong hàm vào đầu danh sách

go
defer fn1()
defer fn2()
defer fn3()

Đoạn code trên tương ứng với hình này

Ngoài goroutine ra, P cũng có liên hệ nhất định với defer, trong cấu trúc của P có một trường deferpool, như dưới đây.

go
type p struct {
  ...
  deferpool    []*_defer // pool of available defer structs (see panic.go)
  deferpoolbuf [32]*_defer
    ...
}

Trong deferpool chứa các cấu trúc defer được phân phối trước, dùng để phân phối cấu trúc defer mới cho goroutine G liên kết với P, có thể giảm bớt chi phí.

Phân phối

Trong cú pháp, việc sử dụng từ khóa defer sẽ được compiler chuyển thành lời gọi đến hàm runtime.deferproc. Ví dụ code go viết như sau

go
defer fn1(x, y)

Mà code thực tế sau khi biên dịch là như sau

go
deferproc(func(){
  fn1(x, y)
})

Nên thực tế hàm mà defer truyền vào không có tham số cũng không có giá trị trả về, hàm deferproc có code như sau

go
func deferproc(fn func()) {
  gp := getg()
  d := newdefer()
  d.link = gp._defer
  gp._defer = d
  d.fn = fn
  d.pc = getcallerpc()
  d.sp = getcallersp()
  return0()
}

Hàm này负责 tạo cấu trúc defer và thêm nó vào đầu danh sách liên kết của goroutine G, trong đó hàm runtime.newdefer sẽ thử lấy cấu trúc defer được phân phối trước từ deferpool trong P.

go
if len(pp.deferpool) == 0 && sched.deferpool != nil {
    lock(&sched.deferlock)
    for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {
        d := sched.deferpool
        sched.deferpool = d.link
        d.link = nil
        pp.deferpool = append(pp.deferpool, d)
    }
    unlock(&sched.deferlock)
}

Nó trước tiên sẽ điền một nửa cấu trúc defer từ deferpool toàn cục sched.deferpool vào deferpool cục bộ, sau đó thử lấy từ deferpool trong P

go
if n := len(pp.deferpool); n > 0 {
    d = pp.deferpool[n-1]
    pp.deferpool[n-1] = nil
    pp.deferpool = pp.deferpool[:n-1]
}

if d == nil {
    // Allocate new defer.
    d = new(_defer)
}
d.heap = true

Cuối cùng nếu thực sự không tìm thấy thì mới sử dụng cách phân phối thủ công. Cuối cùng có thể thấy có một đoạn code như sau

go
d.heap = true

Điều này biểu thị defer được phân phối trên heap, tương ứng khi nó là false thì sẽ được phân phối trên stack, bộ nhớ phân phối trên stack sẽ tự động thu hồi khi trả về, hiệu suất quản lý bộ nhớ của nó cao hơn so với trên heap, và yếu tố quyết định có phân phối trên stack hay không là số tầng vòng lặp, phần logic này có thể truy ngược đến phương thức escape.goDeferStmt trong cmd/compile/ssagen như sau

go
func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
  ...
  if n.Op() == ir.ODEFER && e.loopDepth == 1 {
    k = e.later(e.discardHole())
    n.SetEsc(ir.EscNever)
  }
    ...
}

e.loopDepth biểu thị số tầng vòng lặp của câu lệnh hiện tại, nếu câu lệnh defer hiện tại không nằm trong vòng lặp, sẽ được phân phối vào stack.

go
case ir.ODEFER:
    n := n.(*ir.GoDeferStmt)
    if s.hasOpenDefers {
      s.openDeferRecord(n.Call.(*ir.CallExpr))
    } else {
      d := callDefer
      if n.Esc() == ir.EscNever {
        d = callDeferStack
      }
      s.callResult(n.Call.(*ir.CallExpr), d)
    }

Nếu được phân phối trên stack thì sẽ trực tiếp tạo cấu trúc defer trên stack, cuối cùng sẽ do hàm runtime.deferprocStack hoàn thành việc tạo cấu trúc defer.

go
if k == callDeferStack {
    // Make a defer struct d on the stack.
    if stksize != 0 {
      s.Fatalf("deferprocStack with non-zero stack size %d: %v", stksize, n)
    }

    t := deferstruct()
    ...
    // Call runtime.deferprocStack with pointer to _defer record.
    ACArgs = append(ACArgs, types.Types[types.TUINTPTR])
    aux := ssa.StaticAuxCall(ir.Syms.DeferprocStack, s.f.ABIDefault.ABIAnalyzeTypes(nil, ACArgs, ACResults))
    callArgs = append(callArgs, addr, s.mem())
    call = s.newValue0A(ssa.OpStaticLECall, aux.LateExpansionResultType(), aux)
    call.AddArgs(callArgs...)
    call.AuxInt = int64(types.PtrSize) // deferprocStack takes a *_defer arg

Hàm deferprocStack có chữ ký như sau

go
func deferprocStack(d *_defer)

Logic tạo cụ thể của nó không khác biệt nhiều so với deferproc, khác biệt chủ yếu là, khi phân phối trên stack thì nguồn gốc của cấu trúc defer là cấu trúc được tạo trực tiếp, còn nguồn gốc của defer phân phối trên heap là hàm new.

Thực thi

Khi hàm sắp trả về hoặc xảy ra panic, sẽ đi vào hàm runtime.deferreturn, nó负责 lấy defer từ danh sách liên kết của goroutine và thực thi.

go
func deferreturn() {
  gp := getg()
  for {
    d := gp._defer
    sp := getcallersp()
    if d.sp != sp {
      return
    }
    fn := d.fn
    d.fn = nil
    gp._defer = d.link
    freedefer(d)
    fn()
  }
}

Đầu tiên sẽ thông qua getcallersp() để lấy stack frame của hàm hiện tại và so sánh với sp trong cấu trúc defer để phán đoán defer có thuộc về hàm hiện tại không, sau đó lấy cấu trúc defer từ đầu danh sách, và sử dụng gp._defer = d.link để thực thi defer tiếp theo, rồi thông qua hàm runtuime.freedefer để giải phóng cấu trúc defer vào pool, cuối cùng gọi fn để thực thi, cứ tiếp tục vòng lặp như vậy cho đến khi thực thi xong tất cả defer thuộc về hàm hiện tại thì kết thúc.

Open-coded

Việc sử dụng defer không phải không có chi phí, mặc dù nó cung cấp sự tiện lợi cho chúng ta về cú pháp, nhưng dù sao nó không phải là lời gọi hàm trực tiếp, ở giữa sẽ trải qua một loạt quá trình, nên vẫn sẽ gây ra tổn hao hiệu suất, nên sau này go official đã thiết kế một phương thức tối ưu hóa——open-coded, nó là một phương thức tối ưu hóa cho defer, tên tiếng Anh gốc là open-coded, trong nước基本上 đều dịch thành开放编码, open ở đây có nghĩa là triển khai, tức là triển khai code của hàm defer vào code của hàm hiện tại, giống như hàm inline. Phương thức tối ưu hóa này có các điều kiện hạn chế sau:

  1. Số lượng defer trong hàm không được vượt quá 8
  2. Tích số lượng của deferreturn không được vượt quá 15
  3. defer không được xuất hiện trong vòng lặp
  4. Không bị vô hiệu hóa tối ưu hóa biên dịch
  5. Không thủ công gọi os.Exit()
  6. Không cần sao chép tham số từ heap

Phần logic phán đoán này có thể truy ngược đến phần code dưới đây của hàm cmd/compile/ssagen.buildssa

go
s.hasOpenDefers = base.Flag.N == 0 && s.hasdefer && !s.curfn.OpenCodedDeferDisallowed()
if s.hasOpenDefers && len(s.curfn.Exit) > 0 {
    s.hasOpenDefers = false
}
if s.hasOpenDefers {
    for _, f := range s.curfn.Type().Results().FieldSlice() {
        if !f.Nname.(*ir.Name).OnStack() {
            s.hasOpenDefers = false
            break
        }
    }
}
if s.hasOpenDefers && s.curfn.NumReturns*s.curfn.NumDefers > 15 {
    s.hasOpenDefers = false
}

Rồi go sẽ tạo một biến số nguyên 8 bit deferBits trong hàm hiện tại để làm bitmap dùng để đánh dấu defer, mỗi bit đánh dấu một cái, số nguyên 8 bit uint8 nhiều nhất biểu thị 8 cái, nếu bit tương ứng là 1, thì defer sau khi tối ưu hóa open-coded tương ứng sẽ được thực thi khi hàm sắp trả về.

Golang by www.golangdev.cn edit