Принцип реализации defer в GO

задняя часть
Принцип реализации defer в GO

Это первый раз, когда я участвую в Gengwen Challenge.20День, подробности о событии уточняйте:Обновить вызов

Принцип реализации defer в GO

Давайте рассмотрим последний обмен и поделимся некоторыми знаниями о канале.

  • Поделился, что такое канал в GO
  • Подробный анализ базовой структуры данных канала
  • Как каналы реализованы в исходном коде GO
  • Основы чаньского чтения и письма
  • Какие исключения будут происходить при закрытии канала,panic
  • Простое применение select

если правильноchanЕсли вы все еще заинтересованы в канале, пожалуйста, проверьте статьюРазделение принципов реализации Чана в GO

Что такое отсрочка?

Давайте взглянемdeferчто это такое

это ключевое слово в GO

Это ключевое слово, которое мы обычно используем для высвобождения ресурсов, вreturnпозвоню ему

Если в программе более одногоdefer, последовательность вызова defer выглядит следующим образомкучаПуть,后进先出 LIFOДа, кстати, пишите сюда

  • куча

Следуйте принципу «последним пришел – первым вышел»

Войдите в стек позже, сначала извлеките стек

Сначала в стек, потом из стека

  • очередь

Следуя принципу «первым пришел — первым вышел», мы можем представить односторонний конвейер, входящий слева и выходящий справа.

первым прибыл, первым обслужен

Приходите позже, уходите позже, не прыгая в очереди

Принцип отсрочки реализации

Давайте сначала сделаем вывод, и давайте немного от души:

  • объявлено в кодеdeferМестоположение при компиляции вставляет функцию с именемdeferproc, вdeferВставьте возвращаемую функцию перед функцией, в которой она находится, а неreturnо, даdeferreturn

специфическийdeferКаков принцип реализации, мы все те же, посмотримdeferЧто лежит в основе структуры данных

существуетsrc/runtime/runtime2.goизtype _defer struct {структура

// A _defer holds an entry on the list of deferred calls.
// If you add a field here, add code to clear it in freedefer and deferProcStack
// This struct must match the code in cmd/compile/internal/gc/reflect.go:deferstruct
// and cmd/compile/internal/gc/ssa.go:(*state).call.
// Some defers will be allocated on the stack and some on the heap.
// All defers are logically part of the stack, so write barriers to
// initialize them are not required. All defers must be manually scanned,
// and for heap defers, marked.
type _defer struct {
   siz     int32 // includes both arguments and results
   started bool
   heap    bool
   // openDefer indicates that this _defer is for a frame with open-coded
   // defers. We have only one defer record for the entire frame (which may
   // currently have 0, 1, or more defers active).
   openDefer bool
   sp        uintptr  // sp at time of defer
   pc        uintptr  // pc at time of defer
   fn        *funcval // can be nil for open-coded defers
   _panic    *_panic  // panic that is running defer
   link      *_defer

   // If openDefer is true, the fields below record values about the stack
   // frame and associated function that has the open-coded defer(s). sp
   // above will be the sp for the frame, and pc will be address of the
   // deferreturn call in the function.
   fd   unsafe.Pointer // funcdata for the function associated with the frame
   varp uintptr        // value of varp for the stack frame
   // framepc is the current pc associated with the stack frame. Together,
   // with sp above (which is the sp associated with the stack frame),
   // framepc/sp can be used as pc/sp pair to continue a stack trace via
   // gentraceback().
   framepc uintptr
}

_deferУдерживая запись в списке отложенных вызовов, посмотрим, что означают параметры приведенной выше структуры данных

tag иллюстрировать
siz Размер памяти аргументов и результатов функции отсрочки
fn Функции, которые необходимо отложить
_panic структура паники отсрочки
link Функции задержки отсрочки в одной и той же сопрограмме будут связаны друг с другом через этот указатель.
heap Выделяется ли он в куче
openDefer Оптимизирован ли он для открытого кодирования?
sp Указатель стека (обычно соответствует сборке)
pc счетчик команд

За ключевым словом defer должна следовать функция, мы должны помнить об этом.

Из описания вышеперечисленных параметров мы можем узнать, чтоdeferСтруктура данных аналогична структуре функции и также имеет следующие три параметра:

  • указатель стека SP
  • Счетчик программ ПК
  • адрес функции

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

этоlinkуказатель, это указатель наdeferГлава односвязного списка, каждый раз, когда мы объявляемdeferкогдаdeferДанные вставляются в позицию заголовка этого односвязного списка,

Затем выполнитеdefer, мы можем предположитьdeferКак ты получил это?

упомянутый ранееdeferПоследний пришел, первый вышел, конечно, этот принцип соблюдается и здесь.deferПри выполнении он извлекается из головы односвязного списка.

Давайте нарисуем картину

объявить в сопрограмме A2индивидуальныйdefer, объявить сначалаdefer test1()

перефразироватьdefer test2()

Видно, что утверждение послеdeferбудет вставлен в начало односвязного списка, первый объявленныйdeferстоял в очереди сзади

Когда мы извлекаем, мы всегда извлекаем голову и выполняем ее до тех пор, пока односвязный список не станет пустым.

Давайте взглянемdeferконкретная реализация

image-20210618144713620

Исходные файлы находятся вsrc/runtime/panic.go, см. функциюdeferproc

// Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this.
//go:nosplit
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
   gp := getg()
   if gp.m.curg != gp {
      // go code on the system stack can't defer
      throw("defer on system stack")
   }

   // the arguments of fn are in a perilous state. The stack map
   // for deferproc does not describe them. So we can't let garbage
   // collection or stack copying trigger until we've copied them out
   // to somewhere safe. The memmove below does that.
   // Until the copy completes, we can only call nosplit routines.
   sp := getcallersp()
   argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
   callerpc := getcallerpc()

   d := newdefer(siz)
   if d._panic != nil {
      throw("deferproc: d.panic != nil after newdefer")
   }
   d.link = gp._defer
   gp._defer = d
   d.fn = fn
   d.pc = callerpc
   d.sp = sp
   switch siz {
   case 0:
      // Do nothing.
   case sys.PtrSize:
      *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
   default:
      memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
   }

   // deferproc returns 0 normally.
   // a deferred func that stops a panic
   // makes the deferproc return 1.
   // the code the compiler generates always
   // checks the return value and jumps to the
   // end of the function if deferproc returns != 0.
   return0()
   // No code can go here - the C return register has
   // been set and must not be clobbered.
}

deferprocРоль:

Создать новую отложенную функциюfn, аргумент равен siz байтам, компилятор преобразует отложенный оператор в паруthisвызов

getcallersp():

получатьdeferprocпредыдущийrspЗначение регистра, способ его реализации одинаков для всех платформ.

//go:noescape
func getcallersp() uintptr // implemented as an intrinsic on all platforms

callerpc := getcallerpc():

иди сюдаrspПосле этого хранить вcallerpc, здесь звонитьdeferprocследующая инструкция

d := newdefer(siz):

d := newdefer(siz)создать новыйdeferструктура, последующий код приведен вdeferназначение члена этой структуры

Мы смотримdeferprocОбщий процесс:

  • ПолучатьdeferprocЗначение предыдущего регистра rsp
  • использоватьnewdeferВыделите объект структуры _defer и поместите его в текущий_deferглава связанного списка
  • Инициализируйте соответствующие параметры члена _defer
  • return0

приди и посмотриnewdeferисходный код

Исходные файлы находятся вsrc/runtime/panic.go, см. функциюnewdefer


// Allocate a Defer, usually using per-P pool.
// Each defer must be released with freedefer.  The defer is not
// added to any defer chain yet.
//
// This must not grow the stack because there may be a frame without
// stack map information when this is called.
//
//go:nosplit
func newdefer(siz int32) *_defer {
	var d *_defer
	sc := deferclass(uintptr(siz))
	gp := getg()
	if sc < uintptr(len(p{}.deferpool)) {
		pp := gp.m.p.ptr()
		if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
			// Take the slow path on the system stack so
			// we don't grow newdefer's stack.
			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)
			})
		}
		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]
		}
	}
	if d == nil {
		// Allocate new defer+args.
		systemstack(func() {
			total := roundupsize(totaldefersize(uintptr(siz)))
			d = (*_defer)(mallocgc(total, deferType, true))
		})
	}
	d.siz = siz
	d.heap = true
	return d
}

newderferРоль:

Обычно используется пул per-P, выделяющийDefer

каждыйdeferбесплатно выпустить. токdeferни к кому не присоединюсьdeferцепь

getg():

Получить указатель структуры текущей сопрограммы

// getg returns the pointer to the current g.
// The compiler rewrites calls to this function into instructions
// that fetch the g directly (from TLS or from the dedicated register).
func getg() *g

pp := gp.m.p.ptr():

Получить P в текущем рабочем потоке

Затем возьмите несколько объектов из глобального пула объектов и передайте их в пул P.

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

Нажмите, чтобы увидеть структуру данных пула, на самом деле члены в нем — это то, о чем мы говорили ранее._deferуказатель

вsched.deferpool[sc]это глобальный пул,pp.deferpool[sc]это местный бассейн

mallocgcВыделить место

Если вышеуказанная операция не получает значение d, используйте ее напрямуюmallocgcПереназначить и настроить соответствующие элементыsizа такжеheap

if d == nil {
		// Allocate new defer+args.
		systemstack(func() {
			total := roundupsize(totaldefersize(uintptr(siz)))
			d = (*_defer)(mallocgc(total, deferType, true))
		})
	}
d.siz = siz
d.heap = true

mallocgcСпециально реализовано вsrc/runtime/malloc.goЕсли вам интересно, можете внимательно посмотреть на этот кусок, сегодня мы не будем заострять внимание на этой функции.

// Allocate an object of size bytes.
// Small objects are allocated from the per-P cache's free lists.
// Large objects (> 32 kB) are allocated straight from the heap.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {}

Посмотрим наконецreturn0

Посмотрим наконецdeferprocРезультат в функции возвращаетсяreturn0()

// return0 is a stub used to return 0 from deferproc.
// It is called at the very end of deferproc to signal
// the calling Go function that it should not jump
// to deferreturn.
// in asm_*.s
func return0()

return0используется изdeferprocвернуть0заглушка

это вdeferprocПоследний вызов функции используется для уведомления вызывающегоGoфункция, на которую он не должен переходитьdeferreturn.

при нормальных обстоятельствахreturn0обычно возвращает 0

Но в исключительных случаяхreturn0функция вернется1, то GO перейдет к выполнениюdeferreturn

просто скажиdeferreturn

deferreturnРоль — это ситуация.deferСвязанный список внутри, вернуть соответствующий буфер или оставить соответствующий пробелGCмелодия переработки

Правила отсрочки в GO

Приведенный выше анализ GOdeferПосле принципа реализации давайте теперь посмотрим на приложение в GO.deferследует соблюдать3Как правило, перечислим:

  • deferСледующая функция называется функцией задержки, и параметры этой функции находятся вdeferКогда заявление объявлено, оно было определено
  • Выполнение функции задержки основано на принципе «последний пришел – первый вышел», я уже много раз упоминал об этом в статье, это впечатление должно быть очень глубоким, оно появилось первым.deferПосле казни, после появленияdeferвыполнить первым
  • Отложенные функции могут повлиять на возвращаемое значение всей функции.

давайте еще объясним, второй пункт выше должен быть прост для понимания, на рисунке выше также показан порядок выполнения

Во-первых, давайте напишем небольшую демонстрацию

Параметры функции задержки находятся вdeferКогда заявление объявлено, оно было определено

func main() {
   num := 1
   defer fmt.Println(num)

   num++

   return
}

Не угадаешь, результат1, вы можете скопировать код и запустить его самостоятельно

Третий пункт тоже демо

Отложенные функции могут повлиять на возвращаемое значение всей функции.

func test3() (res int) {
   defer func() {
      res++
   }()

   return 1
}
func main() {

   fmt.Println(test3())

   return
}

В приведенном выше коде мыtest3Возвращаемое значение в функции, мы назвали его заранее, оно должно было возвращать результат как1

но вreturnЗдесь порядок выполнения такой

res = 1

res++

Итак, результат2

Суммировать

  • поделился, что такое отсрочка
  • Простая иллюстрация стеков и очередей
  • Структура данных и принцип реализации defer, отображение конкретного исходного кода
  • отложить в GO3правила

Добро пожаловать лайк, подписка, избранное

Друзья, ваша поддержка и поощрение мотивируют меня продолжать делиться и улучшать качество.

Хорошо, вот и на этот раз,В следующий раз поиграем в капчу с GO

Технологии открыты, и наш менталитет должен быть открытым. Примите перемены, живите на солнце и двигайтесь вперед.

ямаленький дьяволенок Нежа, добро пожаловать, лайкайте, подписывайтесь и добавляйте в избранное, увидимся в следующий раз~