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

news2024/10/5 17:25:13

使用的go版本为 go1.21.2

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

package main

import "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 // 与栈帧相关的函数的 funcdata
    varp     uintptr        // 栈帧的 varp 值
    framepc  uintptr        // 栈帧关联的当前 pc
}

 

deferreturn源码与解读

//代码在GOROOT/src/runtime/panic.go中
func deferreturn() {
    gp := getg() //获取当前运行G

    for {
        //逐步获取当前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.link
            freedefer(d)
            return
        }

        // 获取_defer中保存的执行函数
        fn := d.fn
        d.fn = nil

        // 从G中移除当前_defer,释放其资源。
        gp._defer = d.link
        freedefer(d)

        // 执行延迟函数。
        fn()
    }
}

freedefer源码与解读

//代码在GOROOT/src/runtime/panic.go中
func freedefer(d *_defer) {
    // _defer 结构的 link 字段设置为 nil
    d.link = nil

    // 如果还存在_panic字段,调用 freedeferpanic 函数
    if d._panic != nil {
        freedeferpanic()
    }

    // 如果调度函数不为 nil,调用 freedeferfn 函数
    if d.fn != nil {
        freedeferfn()
    }

    // 如果不在堆上,直接返回
    if !d.heap {
        return
    }
    // 通过当前G的m字段去拿到对应的M
    mp := acquirem()
    // 获取与M绑定的P
    pp := mp.p.ptr()

    // 如果P中的本地缓存已满
    // 将一半的defer池放入到调度器中去
    // 调度器相当于全局池,具体使用是有锁,所以优先使用本地池
    if len(pp.deferpool) == cap(pp.deferpool) {
        var first, last *_defer
        for len(pp.deferpool) > cap(pp.deferpool)/2 {
            n := len(pp.deferpool)
            d := pp.deferpool[n-1]
            pp.deferpool[n-1] = nil
            pp.deferpool = pp.deferpool[:n-1]
            if first == nil {
                first = d
            } else {
                last.link = d
            }
            last = d
        }
        // 获取调度器中的defer锁
        lock(&sched.deferlock)
        //放入到全局池
        last.link = sched.deferpool
        sched.deferpool = first
        //释放调度器中的defer锁
        unlock(&sched.deferlock)
    }

    // 将 _defer 结构清零
    *d = _defer{}

    // 将 _defer 结构放回P的本地缓存
    pp.deferpool = append(pp.deferpool, d)

    // 释放 M
    releasem(mp)
    mp, pp = nil, nil
}

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

deferproc源码与解读

//代码在GOROOT/src/runtime/panic.go中
func deferproc(fn func()) {
    // 获取当前G
    gp := getg()

    // 检查G是否在系统栈上
    if gp.m.curg != gp {
        // 系统栈上的 Go 代码不能使用 defer
        throw("defer on system stack")
    }

    // 创建一个新的 defer 结构
    d := newdefer()

    // 检查新创建的 defer 结构的 _panic 字段是否为 nil
    if d._panic != nil {
        throw("deferproc: d.panic != nil after newdefer")
    }

    // 将新的defer结构添加到当前G的defer链表中
    d.link = gp._defer
    gp._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字段去拿到对应的M
    mp := acquirem()

    // 获取与M绑定的P
    pp := 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.deferpool
            sched.deferpool = d.link
            d.link = nil
            pp.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] = nil
        pp.deferpool = pp.deferpool[:n-1]
    }

    // 释放 M
    releasem(mp)
    mp, pp = nil, nil

    // 如果没有找到可用的 defer 结构体,则分配一个新的
    if d == nil {
        d = new(_defer)
    }

    // 将 'heap' 字段设置为 true 并返回 defer 结构体
    d.heap = true
    return d
}

总结

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度解读英伟达新一轮对华特供芯片H20、L20、L2的定位

大家好&#xff0c;我是极智视界&#xff0c;欢迎关注我的公众号&#xff0c;获取我的更多前沿科技分享 邀您加入我的知识星球「极智视界」&#xff0c;星球内有超多好玩的项目实战源码和资源下载&#xff0c;链接&#xff1a;https://t.zsxq.com/0aiNxERDq 因为一直从事 AI 工…

统计二叉树中的伪回文路径 : 用位运用来加速??

题目描述 这是 LeetCode 上的 「1457. 二叉树中的伪回文路径」 &#xff0c;难度为 「中等」。 Tag : 「DFS」、「位运算」 给你一棵二叉树&#xff0c;每个节点的值为 1 到 9 。 我们称二叉树中的一条路径是 「伪回文」的&#xff0c;当它满足&#xff1a;路径经过的所有节点值…

python之pyqt专栏3-QT Designer

从前面两篇文章python之pyqt专栏1-环境搭建与python之pyqt专栏2-项目文件解析&#xff0c;我们对QT Designer有基础的认识。 QT Designer用来创建UI界面&#xff0c;保存的文件是"xxx.ui"文件&#xff0c;"xxx.ui"可以被pyuic转换为"xxx.py",而&…

文章解读与仿真程序复现思路——电网技术EI\CSCD\北大核心《基于Fisher时段划分的配电网源网荷储多时间尺度协调优化调控策略》

这个标题涉及到电力系统领域的一些关键概念和方法。让我们逐步解读&#xff1a; 基于Fisher时段划分&#xff1a; "基于"表示这个策略或方法的核心基础是某个特定的理论或技术。"Fisher时段划分"可能指的是使用Fisher信息矩阵进行时间划分。Fisher信息矩阵…

php的字符转义函数有那些,是干什么的

在 PHP 中&#xff0c;字符转义函数是用于处理字符串中的特殊字符&#xff0c;以防止这些字符被误解、滥用或引起安全问题的一组函数。这些函数的主要作用是确保在将用户提供的数据插入到数据库、构建 HTML 输出或进行其他与安全相关的操作时&#xff0c;不会导致潜在的安全漏洞…

基于Python 中创建 Sentinel-2 RGB 合成图像

一、前言 下面的python代码将带您了解如何从原始 Sentinel-2 图像创建 RGB 合成图像的过程。 免费注册后&#xff0c;可以从 Open Access Hub 下载原始图像。 请注意&#xff0c;激活您的帐户可能需要 24 小时&#xff01; 二、准备工作 &#xff08;1&#xff09;导入必要的库…

内测分发平台如何保护用户隐私?

大家好&#xff0c;我是咕噜-凯撒&#xff0c;在软件开发的早期阶段&#xff0c;内测是一个至关重要的步骤。通过内测&#xff0c;开发者可以在产品正式上市前发现并修复bug&#xff0c;获取用户反馈优化用户体验。但是内测过程中往往会处理大量用户的敏感信息&#xff0c;尤其…

数字人直播系统开发要注意的陷阱

数字人做为元宇宙的底层基座&#xff0c;BAT都在跑步进场&#xff0c;目前具有前瞻性的公司都在布局数字人产业。数字人可以应用于很多业务场景&#xff0c;对今年来说&#xff0c;无疑数字人直播系统是最火的。像去年数字人直播SAAS系统定制开发的话没有个百把万是下不来的。但…

激光塑料透光率检测仪进行材料质量监控

焊接质量检测是对焊接成果的检测&#xff0c;目的是保证焊接结构的完整性、可靠性、安全性和使用性。焊接质量检测通常包括外观检验、内部检查、无损检测以及试件制作与送检等步骤。通过这些检测方法&#xff0c;可以全面评估焊接质量&#xff0c;确保其符合设计要求和规范标准…

2023年亚太杯数学建模A题水果采摘机器人的图像识别功能(基于yolov5的苹果分割)

注&#xff1a;.题中附录并没有给出苹果的标签集&#xff0c;所以需要我们自己通过前4问得到训练的标签集&#xff0c;采用的是yolov5 7.0 版本&#xff0c;该版本带分割功能 一&#xff1a;关于数据集的制作&#xff1a; clc; close all; clear; %-----这个是生成yolov5 数据…

React UI界面:Ant Design初步

文章目录 初步回调函数变量输出 React初步 初步 Antd是一套非常现代的React组件库&#xff0c;是好多人用过的第一个组件库&#xff0c;但我对其印象最深的却是圣诞节彩蛋&#xff0c;只是上网一查才发现&#xff0c;一晃这么多年过去了。 先创建一个React项目&#xff0c;然…

个人硬件测试用例入门设计

&#x1f4d1;打牌 &#xff1a; da pai ge的个人主页 &#x1f324;️个人专栏 &#xff1a; da pai ge的博客专栏 ☁️宝剑锋从磨砺出&#xff0c;梅花香自苦寒来 &#x1f324;️功能测试 进行新增、…

【开源】基于微信小程序的智慧家政系统

项目编号&#xff1a; S 063 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S063&#xff0c;文末获取源码。} 项目编号&#xff1a;S063&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 查询家政服…

【opencv】计算机视觉:实时目标追踪

目录 前言 解析 深入探究 前言 目标追踪技术对于民生、社会的发展以及国家军事能力的壮大都具有重要的意义。它不仅仅可以应用到体育赛事当中目标的捕捉&#xff0c;还可以应用到交通上&#xff0c;比如实时监测车辆是否超速等&#xff01;对于国家的军事也具有一定的意义&a…

管理类联考——数学——汇总篇——知识点突破——代数——函数——记忆

文章目录 整体文字提炼图像绘画 考点记忆/考点汇总——按大纲 本篇思路&#xff1a;根据各方的资料&#xff0c;比如名师的资料&#xff0c;按大纲或者其他方式&#xff0c;收集/汇总考点&#xff0c;即需记忆点&#xff0c;在通过整体的记忆法&#xff0c;比如整体信息很多&am…

taro h5 ios解决input不能自动获取焦点拉起键盘

描述&#xff1a;页面中有个按钮&#xff0c;点击跳转到第二个页面&#xff08;有input&#xff09;&#xff0c;能直接获取焦点拉起键盘输入 安卓&#xff1a; 直接用focus() ios&#xff1a; focus无效&#xff0c;必须手动拉起 原理&#xff1a; 点击按钮的时候拉起一…

服务器被入侵了怎么去排查

在当今数字化时代&#xff0c;网络安全问题变得越来越重要。其中&#xff0c;服务器被入侵是一种常见的安全威胁。当服务器被入侵时&#xff0c;我们需要采取一系列措施来排查和解决问题。本文将为您提供服务器被入侵后的排查步骤。 第一步&#xff1a;确认服务器被入侵 当发现…

某软件商店app抓包分析与sign加密算法实现

文章目录 1. 写在前面2. 抓包配置3. 抓包分析4. 接口测试5. sign加密算法6. 数据效果展示 【作者主页】&#xff1a;吴秋霖 【作者介绍】&#xff1a;Python领域优质创作者、阿里云博客专家、华为云享专家。长期致力于Python与爬虫领域研究与开发工作&#xff01; 【作者推荐】…

递归算法学习——二叉树的伪回文路径

1&#xff0c;题目 给你一棵二叉树&#xff0c;每个节点的值为 1 到 9 。我们称二叉树中的一条路径是 「伪回文」的&#xff0c;当它满足&#xff1a;路径经过的所有节点值的排列中&#xff0c;存在一个回文序列。 请你返回从根到叶子节点的所有路径中 伪回文 路径的数目。 示例…

【vue_2】创建一个弹出权限不足的提示框

定义了一个名为 getUserRole 的 JavaScript 函数&#xff0c;该函数接受一个参数 authorityId&#xff0c;根据这个参数的不同值返回相应的用户角色字符串。这段代码的目的是根据传入的 authorityId 值判断用户的角色&#xff0c;然后返回相应的角色名称。 如果 authorityId 的…