Skip to content

defer

defer — это часто используемое ключевое слово в повседневной разработке на Go. Оно выполняет функции, связанные с defer, в порядке LIFO (Last In First Out). Мы часто используем этот механизм для операций освобождения ресурсов, таких как закрытие файлов.

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

Учитывая, как часто появляется это ключевое слово, нам необходимо понять лежащую в его основе структуру.

Структура

Ключевое слово defer соответствует структуре runtime._defer. Её структура не сложна:

go
type _defer struct {
  started bool
  heap    bool
  openDefer bool
  sp        uintptr // sp во время defer
  pc        uintptr // pc во время defer
  fn        func()  // может быть nil для open-coded defers
  _panic    *_panic // panic, который выполняет defer
  link      *_defer // следующий defer на G; может указывать на кучу или стек!
  fd   unsafe.Pointer // funcdata для функции, связанной с фреймом
  varp uintptr        // значение varp для стекового фрейма
  framepc uintptr
}

Поле fn — это функция, соответствующая ключевому слову defer, link представляет следующий связанный defer, а sp и pc записывают информацию о функции вызывающего, используемую для определения, какому функции принадлежит defer. defer существует как связный список во время выполнения, с головой списка на goroutine G, поэтому defer фактически напрямую связан с goroutine.

go
type g struct {
    ...
  _panic    *_panic // внутренний panic - смещение известно liblink
  _defer    *_defer // внутренний defer
    ...
}

Когда goroutine выполняет функцию, она последовательно добавляет defer из функции в голову связного списка.

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

Приведённый выше код соответствует этой диаграмме:

Помимо goroutines, P также имеет определённое отношение к defer. В структуре P есть поле deferpool, как показано ниже.

go
type p struct {
  ...
  deferpool    []*_defer // пул доступных структур defer (см. panic.go)
  deferpoolbuf [32]*_defer
    ...
}

deferpool содержит предварительно выделенные структуры defer, используемые для выделения новых структур defer для связанных goroutine G с P, что может уменьшить накладные расходы.

Выделение

С точки зрения синтаксиса, при использовании ключевого слова defer компилятор транслирует это в вызов функции runtime.deferproc. Например, если код Go написан так:

go
defer fn1(x, y)

Фактически скомпилированный код выглядит так:

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

Таким образом, в реальности функция, передаваемая в defer, не имеет параметров и возвращаемых значений. Код функции deferproc следующий:

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()
}

Эта функция отвечает за создание структуры defer и добавление её в голову связного списка goroutine G. Функция runtime.newdefer пытается получить предварительно выделенную структуру defer из deferpool в 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)
}

Сначала заполняет половину локального deferpool из глобального sched.deferpool, затем пытается получить из deferpool 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 {
    // Выделяем новый defer.
    d = new(_defer)
}
d.heap = true

Только когда ничего не может быть найдено, будет использоваться ручное выделение. Наконец, вы можете видеть этот код:

go
d.heap = true

Это указывает, что defer выделяется в куче. Соответственно, когда оно false, оно выделяется в стеке. Память, выделенная в стеке, автоматически возвращается при возврате, и её эффективность управления памятью выше, чем в куче. Фактором, определяющим, выделять ли в стеке, является количество уровней цикла. Эта логика может быть прослежена до метода escape.goDeferStmt в cmd/compile/ssagen:

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 представляет количество уровней цикла для текущего оператора. Если текущий оператор defer не находится в цикле, он будет выделен в стеке.

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)
    }

Если выделено в стеке, структура defer создаётся напрямую в стеке, и в конечном итоге функция runtime.deferprocStack завершает создание структуры defer.

go
if k == callDeferStack {
    // Создаём структуру defer d в стеке.
    if stksize != 0 {
      s.Fatalf("deferprocStack with non-zero stack size %d: %v", stksize, n)
    }

    t := deferstruct()
    ...
    // Вызываем runtime.deferprocStack с указателем на запись _defer.
    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 принимает аргумент *_defer

Сигнатура функции deferprocStack следующая:

go
func deferprocStack(d *_defer)

Конкретная логика создания не сильно отличается от deferproc. Основное различие в том, что при выделении в стеке структура defer происходит из напрямую созданной структуры, в то время как defer, выделенный в куче, происходит из функции new.

Выполнение

Когда функция собирается вернуться или когда происходит panic, входит в функцию runtime.deferreturn, которая отвечает за взятие defer из связного списка goroutine и его выполнение.

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()
  }
}

Сначала использует getcallersp() для получения текущего стекового фрейма функции и сравнивает его с sp в структуре defer для определения, принадлежит ли defer текущей функции. Затем берёт структуру defer из головы связного списка, выполняет следующий defer используя gp._defer = d.link, освобождает структуру defer обратно в пул через функцию runtuime.freedefer, и наконец вызывает fn для выполнения. Это продолжается в цикле, пока все defer, принадлежащие текущей функции, не будут выполнены.

Open-Coded Defer

Использование defer не без затрат. Хотя оно предоставляет удобство в синтаксисе, это не прямой вызов функции и проходит через серию процессов, поэтому действительно вызывает накладные расходы на производительность. Поэтому чиновники Go позже разработали метод оптимизации — open-coded defer. Это оптимизация для defer, первоначально названная "open-coded", которая переводится как "开放编码" на китайском. Здесь "open" означает раскрыть, т.е. раскрыть код функции defer в текущий код функции, подобно встраиванию функции. Этот метод оптимизации имеет следующие ограничения:

  1. Количество defer в функции не может превышать 8
  2. Произведение количества defer и return не может превышать 15
  3. defer не может появляться в цикле
  4. Оптимизация компилятора не отключена
  5. Нет ручного вызова os.Exit()
  6. Не нужно копировать параметры из кучи

Эта логика суждения может быть прослежена до следующей части функции 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
}

Затем Go создаёт 8-битную целочисленную переменную deferBits в текущей функции, служащую битовой картой для маркировки defer. Каждый бит маркирует один defer. 8-битное целое uint8 может представлять максимум 8 defer. Если соответствующий бит равен 1, соответствующий open-coded оптимизированный defer будет выполнен при возврате функции.

Golang by www.golangdev.cn edit