Skip to content

defer

defer ist ein im täglichen Go-Entwicklungsprozess sehr häufig verwendetes Schlüsselwort. Es führt die mit defer verknüpfte Funktion in LIFO-Reihenfolge aus. Oft nutzen wir diesen Mechanismus für die Freigabe von Ressourcen, wie beispielsweise das Schließen von Dateien.

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

Aufgrund der hohen Häufigkeit dieses Schlüsselworts ist es notwendig, die Struktur dahinter zu verstehen.

Struktur

Das defer-Schlüsselwort entspricht der Struktur runtime._defer, deren Aufbau nicht komplex ist:

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
}

Das Feld fn ist die mit dem defer-Schlüsselwort verknüpfte Funktion, link zeigt auf den nächsten verknüpften defer, und sp sowie pc speichern die Funktionsinformationen des Aufrufers, um zu bestimmen, zu welcher Funktion der defer gehört. Zur Laufzeit existiert defer als verkettete Liste, deren Kopf sich auf der Goroutine G befindet, daher ist defer tatsächlich direkt mit der Goroutine verknüpft.

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

Wenn eine Goroutine eine Funktion ausführt, werden die defer in der Funktion in der Reihenfolge am Kopf der verketteten Liste eingefügt:

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

Der obige Code entspricht dieser Abbildung:

Neben der Goroutine ist auch P mit defer verknüpft. In der Struktur von P gibt es ein Feld deferpool, wie folgt:

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

Im deferpool werden vorallozierte defer-Strukturen gespeichert, die verwendet werden, um der mit P verknüpften Goroutine G neue defer-Strukturen zuzuweisen, was den Overhead reduziert.

Zuweisung

Bei der syntaktischen Verwendung des defer-Schlüsselworts wandelt der Compiler dies in einen Aufruf der Funktion runtime.deferproc um. Wenn der Go-Code beispielsweise so geschrieben ist:

go
defer fn1(x, y)

sieht der kompilierte Code tatsächlich so aus:

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

Die an defer übergebene Funktion hat also tatsächlich keine Parameter und keinen Rückgabewert. Der Code der Funktion deferproc lautet:

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

Diese Funktion ist dafür verantwortlich, die defer-Struktur zu erstellen und sie am Kopf der verketteten Liste der Goroutine G einzufügen. Die Funktion runtime.newdefer versucht dabei, eine vorallozierte defer-Struktur aus dem deferpool von P zu erhalten.

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

Zuerst wird die Hälfte der defer-Strukturen aus dem globalen sched.deferpool in den lokalen deferpool übertragen, und dann wird versucht, aus dem deferpool von P zu erhalten:

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

Erst wenn wirklich nichts gefunden wird, wird eine manuelle Zuweisung verwendet. Schließlich ist folgender Code zu sehen:

go
d.heap = true

Dies bedeutet, dass defer auf dem Heap zugewiesen wird. Wenn der Wert false ist, wird es auf dem Stack zugewiesen. Der auf dem Stack zugewiesene Speicher wird bei der Rückgabe automatisch freigegeben, was speicherverwaltungstechnisch effizienter ist als auf dem Heap. Der entscheidende Faktor für die Zuweisung auf dem Stack ist die Schleifentiefe. Diese Logik lässt sich in der Methode escape.goDeferStmt in cmd/compile/ssagen nachverfolgen:

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 gibt die Schleifentiefe der aktuellen Anweisung an. Wenn die aktuelle defer-Anweisung nicht in einer Schleife steht, wird sie auf dem Stack zugewiesen.

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

Wenn auf dem Stack zugewiesen wird, wird die defer-Struktur direkt auf dem Stack erstellt. Schließlich wird die Erstellung der defer-Struktur von der Funktion runtime.deferprocStack durchgeführt:

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

Die Signatur der Funktion deferprocStack lautet:

go
func deferprocStack(d *_defer)

Die konkrete Erstellungslogik unterscheidet sich nicht wesentlich von deferproc. Der Hauptunterschied besteht darin, dass bei der Zuweisung auf dem Stack die defer-Struktur direkt erstellt wird, während bei der Zuweisung auf dem Heap die defer-Struktur über die new-Funktion stammt.

Ausführung

Wenn eine Funktion zurückkehren soll oder ein panic auftritt, wird die Funktion runtime.deferreturn aufgerufen. Sie ist dafür verantwortlich, defer aus der verketteten Liste der Goroutine zu entnehmen und auszuführen.

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

Zuerst wird durch getcallersp() der aktuelle Stack-Frame der Funktion abgerufen und mit dem sp in der defer-Struktur verglichen, um zu bestimmen, ob der defer zur aktuellen Funktion gehört. Dann wird die defer-Struktur vom Kopf der Liste entnommen, mit gp._defer = d.link auf den nächsten defer verwiesen, die defer-Struktur durch die Funktion runtime.freedefer an den Pool zurückgegeben und schließlich fn aufgerufen. Dieser Prozess wird so lange wiederholt, bis alle zur aktuellen Funktion gehörenden defer ausgeführt wurden.

Open-Coded Defers

Die Verwendung von defer ist nicht kostenlos. Obwohl sie uns syntaktisch Vorteile bietet, ist sie kein direkter Funktionsaufruf. Dazwischen liegt eine Reihe von Prozessen, die dennoch zu Leistungseinbußen führen. Später entwarf das offizielle Go-Team eine Optimierungsmethode - Open-Coded Defers. Es handelt sich um eine Optimierung für defer, deren ursprünglicher englischer Name "open-coded" lautet. Das "open" bedeutet hier "entfalten" - der Code der defer-Funktion wird in den aktuellen Funktionscode entfaltet, ähnlich wie bei Function Inlining. Diese Optimierungsmethode hat folgende Einschränkungen:

  1. Die Anzahl der defer in einer Funktion darf 8 nicht überschreiten
  2. Das Produkt aus der Anzahl von defer und return darf 15 nicht überschreiten
  3. defer darf nicht in einer Schleife erscheinen
  4. Compiler-Optimierungen sind nicht deaktiviert
  5. os.Exit() wurde nicht manuell aufgerufen
  6. Parameter müssen nicht vom Heap kopiert werden

Diese Entscheidungslogik lässt sich in folgendem Teil der Funktion cmd/compile/ssagen.buildssa nachverfolgen:

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
}

Dann erstellt Go in der aktuellen Funktion eine 8-Bit-Integer-Variable deferBits als Bitmap zur Markierung von defer. Jedes Bit markiert einen defer, ein 8-Bit-Integer uint8 kann maximal 8 darstellen. Wenn das entsprechende Bit 1 ist, wird der entsprechend optimierte defer ausgeführt, wenn die Funktion zurückkehrt.

Golang by www.golangdev.cn edit