golang defer关键词执行原理与代码分析

使用的go版本为 go1.21.2

首先我们写一个简单的defer调度代码

package mainimport "fmt"func main() {defer func() {fmt.Println("xiaochuan")}()
}

通过go build -gcflags -S main.go获取到对应的汇编代码

可以在图中看到有个CALL runtime.deferreturn(SB) 调度这个是编译器插入的defer执行调度

我们先来看一下defer构造体的底层源码

defer结构体

//代码在GOROOT/src/runtime/runtime2.go中type _defer struct {started   bool        // 表示是否已经开始执行heap      bool        // 标志是否分配在堆上openDefer bool        // 标志是否对应于一个带有 open-coded defers 的栈帧sp        uintptr     // 执行时的栈指针pc        uintptr     // 执行时的程序计数器fn        func()      // 存储被延迟执行的函数_panic    *_panic     // 当前执行的 panic(如果有的话)link      *_defer     // 在G(goroutine)上指向下一个延迟结构,可以指向堆或栈// 如果 openDefer 为 true,则以下字段记录与具有 open-coded defers 的栈帧相关的值。// 在这种情况下,sp 字段上面的 sp 是栈帧的 sp,而 pc 是关联函数中 deferreturn 调用的地址。fd       unsafe.Pointer // 与栈帧相关的函数的 funcdatavarp     uintptr        // 栈帧的 varp 值framepc  uintptr        // 栈帧关联的当前 pc
}

 

deferreturn源码与解读

//代码在GOROOT/src/runtime/panic.go中
func deferreturn() {gp := getg() //获取当前运行Gfor {//逐步获取当前G中的defer调用d := gp._defer// 如果获取到的构造体为空,直接返回。if d == nil {return}// 获取调用 defer 语句的函数的栈指针。sp := getcallersp()// 如果_defer里面存的栈指针与当前函数的栈指针不匹配,直接返回。// 说明数据存在改写不给予处理if d.sp != sp {return}// 如果_defer使用了 open-coded defers(编码的延迟调用)if d.openDefer {// 运行 open-coded defers 的帧。done := runOpenDeferFrame(d)// 如果 open-coded defers 没有完成,抛出异常。if !done {throw("unfinished open-coded defers in deferreturn")}// 将_defer从G的延迟链表移除,释放对应的_defer构造体资源gp._defer = d.linkfreedefer(d)return}// 获取_defer中保存的执行函数fn := d.fnd.fn = nil// 从G中移除当前_defer,释放其资源。gp._defer = d.linkfreedefer(d)// 执行延迟函数。fn()}
}

freedefer源码与解读

//代码在GOROOT/src/runtime/panic.go中
func freedefer(d *_defer) {// _defer 结构的 link 字段设置为 nild.link = nil// 如果还存在_panic字段,调用 freedeferpanic 函数if d._panic != nil {freedeferpanic()}// 如果调度函数不为 nil,调用 freedeferfn 函数if d.fn != nil {freedeferfn()}// 如果不在堆上,直接返回if !d.heap {return}// 通过当前G的m字段去拿到对应的Mmp := acquirem()// 获取与M绑定的Ppp := mp.p.ptr()// 如果P中的本地缓存已满// 将一半的defer池放入到调度器中去// 调度器相当于全局池,具体使用是有锁,所以优先使用本地池if len(pp.deferpool) == cap(pp.deferpool) {var first, last *_deferfor len(pp.deferpool) > cap(pp.deferpool)/2 {n := len(pp.deferpool)d := pp.deferpool[n-1]pp.deferpool[n-1] = nilpp.deferpool = pp.deferpool[:n-1]if first == nil {first = d} else {last.link = d}last = d}// 获取调度器中的defer锁lock(&sched.deferlock)//放入到全局池last.link = sched.deferpoolsched.deferpool = first//释放调度器中的defer锁unlock(&sched.deferlock)}// 将 _defer 结构清零*d = _defer{}// 将 _defer 结构放回P的本地缓存pp.deferpool = append(pp.deferpool, d)// 释放 Mreleasem(mp)mp, pp = nil, nil
}

 看老的版本的一些文章介绍在使用 defer func(){}() 时编译器会将转换为runtime.deferproc Go新版本没看到汇编对应的调度过程,希望有大哥能帮忙解答一下新版本是如何调度到runtime.deferproc函数

deferproc源码与解读

//代码在GOROOT/src/runtime/panic.go中
func deferproc(fn func()) {// 获取当前Ggp := getg()// 检查G是否在系统栈上if gp.m.curg != gp {// 系统栈上的 Go 代码不能使用 deferthrow("defer on system stack")}// 创建一个新的 defer 结构d := newdefer()// 检查新创建的 defer 结构的 _panic 字段是否为 nilif d._panic != nil {throw("deferproc: d.panic != nil after newdefer")}// 将新的defer结构添加到当前G的defer链表中d.link = gp._defergp._defer = d// 设置defer触发函数d.fn = fn//GOROOT/src/runtime/stubs.go//注释是这么说的返回其调用者的调用者的程序计数器//具体实现在汇编层d.pc = getcallerpc()//GOROOT/src/runtime/stubs.go//注释是这么说的返回其调用者的调用者的堆栈指针//具体实现在汇编层d.sp = getcallersp()//GOROOT/src/runtime/stubs.go//return0 是一个用于从 deferproc 返回 0 的存根。//它在 deferproc 的最后调用来发出信号//调用 Go 函数时不应跳转//推迟返回。//具体实现在汇编层return0()// 不能在这里放置代码 - C 返回寄存器已设置,不能被破坏。
}

 

newdefer源码与解读

//代码在GOROOT/src/runtime/panic.go中
func newdefer() *_defer {// 声明一个_defer指针变量var d *_defer// 通过当前G的m字段去拿到对应的Mmp := acquirem()// 获取与M绑定的Ppp := mp.p.ptr()// 检查P中 deferpool 是否为空,且调度器中有可用的 defer 结构体if len(pp.deferpool) == 0 && sched.deferpool != nil {// 获取调度器中的defer锁lock(&sched.deferlock)// 将调度器中的deferpool转移到P的本地池中去for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {d := sched.deferpoolsched.deferpool = d.linkd.link = nilpp.deferpool = append(pp.deferpool, d)}// 释放调度器中的defer锁unlock(&sched.deferlock)}// 检查P的本地池中是否有可用的defer结构体if n := len(pp.deferpool); n > 0 {// 从本地池拿出来一个 defer 结构体d = pp.deferpool[n-1]pp.deferpool[n-1] = nilpp.deferpool = pp.deferpool[:n-1]}// 释放 Mreleasem(mp)mp, pp = nil, nil// 如果没有找到可用的 defer 结构体,则分配一个新的if d == nil {d = new(_defer)}// 将 'heap' 字段设置为 true 并返回 defer 结构体d.heap = truereturn d
}

总结

从上面的源码我们可以了解到defer的大致逻辑,当使用defer关键词时,会将当前要延迟的函数加入到G的延迟链表中去,当我们的函数执行完成后会触发deferreturn调度将G中的延迟链表循环执行一遍,来达到延迟执行的目的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/216295.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2、用命令行编译Qt程序生成可执行文件exe

一、创建源文件 1、新建一个文件夹&#xff0c;并创建一个txt文件 2、重命名为main.cpp 3、在main.cpp中添加如下代码 #include <QApplication> #include <QDialog> #include <QLabel> int main(int argc, char *argv[]) { QApplication a(argc, argv); QDi…

Edit And Resend测试接口工具(浏览器上的Postman)

优点 可以不用设置Cookie或者Token&#xff0c;只设置参数进行重发接口测试API 使用Microsoft Rdge浏览器 F12——然后点击网络——在页面点击发起请求——然后选择要重发的请求右键选择Edit And Resend——在网络控制台设置自己要设置的参数去测试自己写的功能

Jquery ajax 进行网络请求,同步阻塞引起的UI线程阻塞 (loading图片不显示 )

jax重新获取数据刷新页面功能&#xff0c;因为ajax属于耗时操作&#xff0c;想在获取数据且加载页面时显示加载遮罩层&#xff0c;结果发现了ajax的好多坑。 ajax 执行http网络请示时时&#xff0c;让遮罩层显示&#xff0c;ajax加载完毕后遮罩层消失。 因为我想让loadChart()…

集「才华」与「美貌」于一身的原型设计利器—摹客RP

文章目录 画原型做设计&#xff0c;用摹客RP就够了 初遇摹客再遇摹客RP摹客RP简介与注册摹客RP的突出亮点1️⃣拥有海量矢量图标&#xff0c;满足各种设计场景2️⃣打造高扩展性组件&#xff0c;打破传统组件编辑模式3️⃣海量摹客RP模板例子随意挑选4️⃣实现多人实时协同&…

laravel8安装多应用多模块(笔记三)

先安装laravel8 Laravel 安装&#xff08;笔记一&#xff09;-CSDN博客 一、进入项目根目录安装 laravel-modules composer require nwidart/laravel-modules 二、 大于laravel5需配置provider&#xff0c;自动生成配置文件 php artisan vendor:publish --provider"Nwid…

软件设计中如何画各类图之二深入解析数据流图(DFD):系统设计与分析的关键视觉工具

目录 1 前言2 数据流图&#xff08;DFD&#xff09;的重要性3 数据流图的符号说明4 清晰的数据流图步骤4.1 确定系统边界4.2 识别数据流4.3 定义处理过程4.4 确认数据存储4.5 建立数据流动的连线4.6 细化和优化 5 数据流图的用途6 使用场景7 实际应用场景举例8 结语 1 前言 当…

手动创建映像及在OpenStack云计算平台的镜像应用

目录 一、下载 rhel7.6 安装ISO 二、在VMware 的虚拟机内创建虚拟机 三、更改一些设置 1、使用httpd暴露&#xff08;在外部虚拟机&#xff09; 2、添加软件仓库 3、 安装 ACPI 服务 4、使用 cloud-init 获取公钥 5、安装 cloud-utils-growpart 以允许调整分区大小 6、…

【Spring Boot 源码学习】自定义 Banner 信息打印

Spring Boot 源码学习系列 自定义 Banner 信息打印 引言往期内容主要内容1. ResourceBanner 打印1.1 添加默认的 banner.txt 资源文件1.2 指定任意路径的资源文件1.3 添加自定义的信息 2. ImageBanner 打印2.1 添加默认的图像资源文件2.2 指定任意路径的图像资源文件2.3 添加自…

idea自动切换输入法Smart Input

idea搜索后下载 红色表示中文输入法 再ideavim场景下会自动切换成英文非常好用强烈推荐下载一个

Kafka 集群如何实现数据同步

Kafka 介绍 Kafka 是一个高吞吐的分布式消息系统&#xff0c;不但像传统消息队列&#xff08;RaabitMQ、RocketMQ等&#xff09;那样能够【异步处理、流量消峰、服务解耦】 还能够把消息持久化到磁盘上&#xff0c;用于批量消费。除此之外由于 Kafka 被设计成分布式系统&…

Nginx模块开发之http handler实现流量统计(2)

文章目录 一、概述二、Nginx handler模块开发2.1、代码实现2.2、编写config文件2.3、编译模块到Nginx源码中2.4、修改conf文件2.5、执行效果 总结 一、概述 上一篇【Nginx模块开发之http handler实现流量统计&#xff08;1&#xff09;】使用数组在单进程实现了IP的流量统计&a…

【【linux C 编程记述 之 VIM的用法讲述】】

linux C 编程记述 之 VIM的用法讲述 我们所说的编写代码包括两部分&#xff1a;代码编写和编译&#xff0c;在Windows下可以使用Visual Studio来完成这两部&#xff0c;可以在 Visual Studio 下编写代码然后直接点击编译就可以了。但是在 Linux 下这两部分是分开的&#xff0c…