惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
CXSECURITY Database RSS Feed - CXSecurity.com
D
Docker
有赞技术团队
有赞技术团队
WordPress大学
WordPress大学
Jina AI
Jina AI
小众软件
小众软件
Last Week in AI
Last Week in AI
Hugging Face - Blog
Hugging Face - Blog
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
美团技术团队
爱范儿
爱范儿
V
V2EX
大猫的无限游戏
大猫的无限游戏
人人都是产品经理
人人都是产品经理
J
Java Code Geeks
博客园 - 司徒正美
博客园 - 叶小钗
S
SegmentFault 最新的问题
量子位
S
Secure Thoughts
月光博客
月光博客
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
O
OpenAI News
L
LINUX DO - 最新话题
罗磊的独立博客
SecWiki News
SecWiki News
雷峰网
雷峰网
Recent Announcements
Recent Announcements
V2EX - 技术
V2EX - 技术
T
Tailwind CSS Blog
H
Hacker News: Front Page
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
云风的 BLOG
云风的 BLOG
Schneier on Security
Schneier on Security
T
The Blog of Author Tim Ferriss
IT之家
IT之家
博客园 - 聂微东
腾讯CDC
N
News | PayPal Newsroom
P
Proofpoint News Feed
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
The GitHub Blog
The GitHub Blog
Hacker News: Ask HN
Hacker News: Ask HN
aimingoo的专栏
aimingoo的专栏
Webroot Blog
Webroot Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google DeepMind News
Google DeepMind News
K
Kaspersky official blog

蛮荆

如何获取更多的免费服务器 Kubernetes 调度器队列 - 设计与实现 Kubernetes 调度器 - 核心流程 Kubernetes Networking Model & CNI Kubernetes 控制器管理总结 Kubernetes CronJob 设计与实现 Kubernetes Job 设计与实现 Kubernetes HPA 设计与实现 Kubernetes Deployment 滚动更新实现原理 Kubernetes GC 设计与实现 Kubernetes Pod 驱逐 - 设计与实现 Kubernetes Daemonset 设计与实现 Kubernetes ReplicaSet 设计与实现 Kubernetes EndPoint 设计与实现 Kubernetes Informer 设计与实现 降本增效之应用优化 (三) 日志存储与检索 Kubernetes Pod 设计与实现 - 创建流程 Kubernetes 探针设计与实现 Unix 编程艺术名句摘录 Kubernetes - CRI 概述 Golang 编译速度为什么这么快? Kubernetes Pod 设计与实现 - Pause 容器 Kubernetes - kube-proxy 代理模式工程优化 Kubernetes 应用最佳实践 - 优雅关闭长连接 Kubernetes Service 类型和会话亲和性 Kubernetes 为什么需要 Ingress Kubernetes 架构 - 控制平面和数据平面 降本增效之应用优化 (二) 大报表 Go 语言如何获取 CPU 利用率 降本增效之应用优化 (一) Redis 业务规则引擎演变过程简述 微服务中的熔断算法 漏桶算法和令牌桶算法 jsonparser 为什么比标准库的 encoding/json 快 10 倍 ? zap 高性能设计与实现 HTTP Router 算法演进 fastcache 高性能设计与实现 Web 常见的三个安全问题 ants Code Reading Go 线程安全 map 方案选型 布隆过滤器 死锁、活锁、饥饿、自旋锁 sync.Pool Code Reading Go 内存管理概述 Go netpoll Code Reading goroutine 泄漏与检测 time/Timer Code Reading GMP Scheduler Code Reading Go channel 的 15 条规则和底层实现 为什么 Linux “一切皆文件” context.Context Code Reading runtime/HACKING.md Goland 最佳实践 互联网开发与金庸武学 为什么 Redis 6.0 引入多线程模型? Kubernetes 应用最佳实践 - 金丝雀发布 容器中如何正确配置 GOMAXPROCS ? singleflight Code Reading sync.Map Code Reading sync.Cond Code Reading sync.WaitGroup Code Reading sync.RWMutex Code Reading sync.Mutex Code Reading sync.Once Code Reading Go 无锁编程 sync/atomic Code Reading goroutine 交替打印奇偶数 GODEBUG Go 并发模式 Go 汇编 UUID 通用技术选型 Kubernetes 应用最佳实践 - 水平自动伸缩 Go 高性能 Tips fasthttp 为什么比标准库 net/http 快 10 倍 ? 技术文章配图指南 ChatGPT 初体验 Docker 网络原理概览 iptables 的五表五链 Kubernetes 应用最佳实践 - 亲和性和污点容忍度 Go 的反射与三大定律 Docker 官方提供的最佳实践 Go 语言内置的设计模式 HTTP1 到 HTTP3 的工程优化 Kubernetes 应用最佳实践 - Sidecar 模式 Kubernetes 应用最佳实践 - init 容器和钩子函数 为什么 recover 必须在 defer 中调用? Go map 设计与实现 Go 切片扩容底层实现 Go 语言中的零拷贝 Go Delve 云原生和边缘计算简介 Kubernetes Pod 服务质量等级 Kubernetes 应用最佳实践 - 探针 Kubernetes 应用最佳实践 - 资源请求和限制 CDN 原理 Kubernetes 应用最佳实践 - 开篇 缓存策略和模式 Go 内存模型 Kubernetes 核心概念 软件版本语义化
为什么 defer 的执行顺序和注册顺序不同?
2023-03-12 · via 蛮荆

2023-03-12 Golang Go 源码分析 读代码

概述

defer 语句的执行顺序是 后进先出,和数据结构中的 Stack (栈) 一样。

package main 

func main() {
    defer func () {
        println(1)
    }()
    
    defer func () {
        println(2)
    }()
    
    defer func () {
        println(3)
    }()
}

defer 函数的注册顺序为 1, 2, 3。

$ go run main.go

# 输出结果如下
3
2
1

defer 函数的执行顺序为 3, 2, 1。

内部实现

为什么最后的执行顺序和最初的注册顺序不一样呢?我们从源代码的角度来探究一下,defer 的实现相关文件目录为 $GOROOT/src/runtime,笔者的 Go 版本为 go1.19 linux/amd64

_defer 对象

_defer 对象表示 defer 语句 的运行时。

type _defer struct {
	started   bool    // defer 语句是否已经执行
	heap      bool    // defer 区分对象是在堆上分配还是栈上分配
	sp        uintptr // 调用方(调用 defer 函数)的 sp (栈底) 寄存器
	pc        uintptr // 调用方(调用 defer 函数)的 pc (程序计数器) 寄存器,下一条汇编指令的地址
	fn        func()  // 传入 defer 的函数,包括函数地址及参数
	_panic    *_panic // 正在执行 defer 的 panic 对象
	link      *_defer // next defer, 链表指针,可以指向栈或者堆
}

_defer 对象链表如下所示:

_defer 对象


deferproc 方法

deferproc 方法用于创建新的 defer 调用,编译器将 defer 语句转换为调用 deferproc 函数,生成对应函数及其参数对应的 _defer 对象,然后将其挂载到当前 G (goroutine) 的 _defer 链表上

func deferproc(fn func()) {
	// 获取当前 G
	gp := getg()

	d := newdefer()
	if d._panic != nil {
        // 新分配的 _defer 不应该产生 panic, 属于异常情况 
		throw("deferproc: d.panic != nil after newdefer")
	}

    // 新分配的 _defer 放在链表头部,达到 "后进先出" 的执行顺序 
	d.link = gp._defer      
	gp._defer = d

    // 将函数赋值到 _defer 的 fn 字段
	d.fn = fn
    // 调用方(调用 defer 函数)的 pc (程序计数器) 寄存器,下一条汇编指令的地址
	d.pc = getcallerpc()    
	
	// 不能在调用 getcallersp 函数和将返回值存储到 d.sp 期间被抢占
	// 因为 getcallersp 函数的返回值是一个 uintptr 栈指针
	d.sp = getcallersp()
	
	// return0 函数由汇编实现
	// 正常情况下返回 0 
	// 如果有捕获 panic 的 defer 函数, 返回 1
	// 编译器生成的代码总是检查返回值
	// 如果 deferproc 返回值不等于 0 (说明 panic 被捕获到了)
	// 如果 deferproc 返回值等于 0 (说明 panic 没有被捕获)
	
	return0()
}

newdefer 方法

newdefer 方法用于创建一个新的 _defer 对象,并返回指向该对象的指针。

newdefer 会尽可能复用已有的 _defer 对象,优先从当前处理器 Pdefer pool 对象池中获取,其次是调度器的 defer pool 对象池, 如果这两个地方都没有获取到,就创建一个新的 _defer 对象

func newdefer() *_defer {
	var d *_defer
	// 获取当前 M
	mp := acquirem()
    // 获取当前 P
	pp := mp.p.ptr()    

	// 如果 P 的对象池为空,并且调度器的对象池不为空
	// 从调度器取 N 个 对象放入 P 的对象池,完成数据局部性缓存 
	// (其中 N 等于 P 的对象池容量的一半)
	if len(pp.deferpool) == 0 && sched.deferpool != nil {
		lock(&sched.deferlock)
		// 将调度器对象池中一半的对象放入 P 的对象池
		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)
	}
	
	// 如果 P 的对象池中有元素,那么取出池中最后一个元素
	if n := len(pp.deferpool); n > 0 {
		d = pp.deferpool[n-1]
		pp.deferpool[n-1] = nil
		pp.deferpool = pp.deferpool[:n-1]
	}

    // 消除当前 M 和 P 的引用 (优化 GC)
	mp, pp = nil, nil

	// 如果 P 的对象池和调度器的对象池都没有获取到对象
	// 那就创建一个新的
	if d == nil {
		d = new(_defer)
	}

    // 标记对象分配到了堆上
	// 和 deferprocStack 函数正好相反 
	d.heap = true   
	return d
}

newdefer 方法执行流程

deferprocStack 方法

deferprocStack 方法用于将栈上的 _defer 对象挂载到当前 G_defer 链表上,参数必须初始化它的 fn 字段,其他字段都可以不设置。 由于栈上的指针未初始化, 所以进入函数之前,就已经在栈上分配好了内存结构 (编译器分配的,rsp 往下伸展即可)。

//go:nosplit
func deferprocStack(d *_defer) {
	// 获取当前 G
	gp := getg()
	
	// fn 字段已经初始化
	// 其他字段刚开始是未初始化的,现在进行初始化
	d.started = false
    // 分配到了栈上, 和 newdefer 函数正好相反
	d.heap = false
    // 获取到调用方函数的 rsp 寄存器值,并赋值到 _defer 结构的 sp 字段中
	d.sp = getcallersp()
    // 获取到调用方函数的 pc 寄存器值,并赋值到 _defer 结构的 pc 字段中
	d.pc = getcallerpc()
	
	// 下面的代码可以翻译为:
	//   d.panic = nil
	//   d.fd = nil
	//   d.link = gp._defer
	//   gp._defer = d

    // 将参数 _defer 对象挂载到当前 G 的 _defer 链表上
	*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
	*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
	*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
	*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))

	return0()
}

deferreturn 方法

deferreturn 方法用于运行 defer 语句函数,编译器在调用 defer 语句的函数末尾插入调用 deferreturn 函数代码。

func deferreturn() {
	// 获取当前 G
	gp := getg()
	
	for {
		// 获取链表头节点
		d := gp._defer
		// 链表已没有元素,直接返回
		if d == nil { 
			return
		}

        // 获取到调用方函数的 rsp 寄存器值
		sp := getcallersp()
		// 如果调用方函数的 sp 和 _defer 对象的 sp 字段不同
		// 说明 _defer 对象不是在调用方函数注册的 (一般是由于跨 goroutine 造成的)
		if d.sp != sp {     
			return
		}

        // 获取 _defer 对象的 fn 函数地址
		fn := d.fn
		// 释放 _defer 对象的 fn 字段
		d.fn = nil
        // 从链表中删除当前节点
		gp._defer = d.link
        // 释放 _defer 对象内存 (归还到 P 的对象池或者调度器的对象池)
		freedefer(d)
		// 执行 defer 语句函数
		fn()                
	}
}

deferreturn 方法执行流程

小结

两个小问题:

  1. 为什么 defer 的执行顺序和注册顺序不同?
  2. 为什么 defer 无法跨 goroutine 捕获 panic

defer 的实现由编译器和运行时共同完成,通过对内部实现源代码的学习,我们现在可以回答上面的两个小问题了。

  1. 新分配的 _defer 结构体对象会挂载到链表头部,达到 后进先出 的执行顺序。
  2. _defer 结构体对象会挂载到当前的 G (goroutine) 上面,形成关联绑定,所以无法跨越 goroutine 的作用域去捕获 panic

扩展阅读