关于 Go 协同程序(Coroutines 协程)、Go 汇编及一些注意事项。
参考:
Go 汇编函数 - Go 语言高级编程
Go 嵌套汇编 - 掘金 (juejin.cn)
前言:
Golang 适用 Go-Runtime(Go 运行时,嵌入在被编译的PE可执行文件之中)来管理调度协同程式的运行。
Go 语言没有多线程(MT)的概念,在 Go 语言之中,每个 Go 协程就类似开辟了一个新的线程,效率上,肯定是比分配线程好的。
但也仅限于分配协程,及单个进程可以跑几万个乃至几十万个协同程序,这是线程无法比拟的,因为在操作系统之中,最小执行单元的单位就是线程了,但是线程相对协同程序来说,过重,无论是内存还是CPU。
但不意味着 Go 协程执行的效率比线程要好,别太自信与盲目,协程是比不了线程代码CPU执行效率的。
上面也提到了,只是可以同时开辟几万个乃至几十万个协程,并且启动协程速度比线程快非常多,这是它的优势,但是缺点也很明显,在物理线程上执行 Go 协同程式的代码效率不高。
目前世界上最快的协同程序切换,应该是 C/C++ 之中的:
State Threads Library (sourceforge.net)
boost::context
两个库各有千秋,但相对来说 boost 更好用一些,在这里需要提醒大家一点,应用程序之中运行协同程序,它是依托于进程之中的物理线程上执行的。
来到正题,我们先来探讨 Golang 到底是 “Stackless” 无栈轻量协程,还是 “Stackful” 有栈重量协程呢?
那么就有必要分析清楚,有栈协程跟无栈协程之间到底有什么区别。
首先:
1、有栈协程
1.1、栈协程是一种基于线程或进程的协程实现方式。
1.2、栈协程拥有自己的执行栈,可以独立地管理栈帧、局部变量和函数调用。
1.3、栈协程的切换需要保存和恢复整个执行上下文,包括栈指针、寄存器等。
1.4、由于栈协程具有独立的执行栈,因此它们可以支持递归调用和深度嵌套。
1.5、由于栈协程需要额外的资源来维护栈,因此在创建和销毁方面可能会有一些开销。
2、无栈协程
2.1、无栈协同是一种基于用户空间的协程实现方式。
2.2、无栈协同没有独立的执行栈,它们共享相同的调用栈。【重点】
2.3、无栈协同使用状态机来管理协程的执行,并通过保存和恢复状态来实现协程的切换。
2.4、由于无栈协同共享调用栈,因此它们不能支持递归调用和深度嵌套。
2.5、无栈协同通常比栈协程更轻量级,创建和销毁开销较小。
似乎从上述定义的概念来说,Golang 是有栈协议?但真的是这样吗?显然不是的,首先真正意义上的有栈协程,是无法被运行时代管的。
有栈协程存在以下几个限制:
1、如果开发人员切换协程处理不当的情况下,会导致协程栈内存泄漏问题。
2、如果开发人员在多个线程之中执行
3、有栈协程无法动态扩展计算栈空间,所以有栈协程需要在分配时,明确指定栈空间大小。
一个协同程序可以在多个线程上按保证顺序性(时序)进行处理,无论是有栈协同程序、或者是无栈协同程序,均可以。
Go 协同程序是属于 “Stackless” 无栈协程的类型,但 Go 为了实现协同程序能像 Stackful 有栈协程一样,拥有属于自己的外挂栈空间,并且支持动态栈空间扩容。
但要注意一点:
1、Go 协程可能在不同的线程上面被执行,虽然 Go 语言运行时保证了,单一协同程序执行的时序性,但开发人员需要在其中注意协同程序之间的同步问题,类似多线程并发编程。
2、若要实现同步锁的情况,人们需要考虑多线程问题,否则这可能造成很严重的后果,即 Go 运行时附着的工作线程被阻塞,同时最好的实现方式伪同步锁,如利用管道来实现类似效果。
相对传统的 TTASLock/CAS自选锁实现,可能不太适合Go 这种结构的程序,这是因为:Go 协同程序在没有执行异步的情况下是不会让出线程CPU的,你可以理解为,你需要执行类似文件IO、网络IO、或者调用 Go 运行时库之中的同步库,例如:sync.Mutex 产生了阻塞行为
鉴于 Go 运行时是多线程执行,在不阻塞 Go 运行时最大工作线程的情况下,其它协程,仍旧是可以正常就绪的工作的,这取决于运行时调度。
所以严格意义上来说,Go 协程属于 “Stackless” + “Stackful” 的变种协程,它属于 “Stackless” 无栈协同程序的一种,但 Go 编译器实现对其用户代码进行展开,并分配一个 “Go 外挂计算栈内存空间单元”,而非真正意义上的函数栈,如同C#、C++、C#、ASM、IL函数的调用堆栈。
有栈协程无法放大执行堆栈的根本原因是寄存器,EIP、RIP,及地址链之间存在上下依赖问题等等,Go 并非是真的有栈协程,自然不会存在这个问题,它本来就是由编译器支持的黑魔法,实现的协同程序(“重点:最终会被展开编译为状态机切换的”),但这类编译器不能编译过度复杂协同应用程序,虽然我个人是相信 Google 的技术水平的,但并不代表,不对 Stackless 协程先天存在的对于编译器的复杂性,感到一丝忧虑,这个世界上不存在完美的技术,这类编译器完全内部实现的纯纯黑盒,对开发人员来说不太容易掌控到更多的细节。
Go 通过外挂计算栈空间的解决方案,在该 Go 栈空间内不保存任何寄存器之类的值,仅存储调用函数栈帧的元RID、参数、变量等(值或引用),所以在栈空间不足时,进行扩大外挂栈时。
即:分配新的栈空间内存,并把原栈内存复制过来,在释放原栈内存空间的内存,并把新的栈内存首地址(指针)挂载到当前 Go 协同程序的栈顶指针、栈底指针。
在复制并放大 Go 协程栈内存空间的时候,会导致该协同被同步阻塞,恢复取决于这个步骤在何时完成。
Go 栈空间虽然不会保存寄存器的值,但并不意味着 Go 程序不会适用目标平台汇编指令集
下述是一个很简单的 Go 加法函数,返回参数 x+y 的值:
package mainfunc Add(x int, y int) int {return x + y
}func main() {}
那么 Go 编译器会输出以下的汇编指令
TEXT main.Add(SB), NOSPLIT|NOFRAME|ABIInternal, $0-16FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)FUNCDATA $5, main.Add.arginfo1(SB)FUNCDATA $6, main.Add.argliveinfo(SB)PCDATA $3, $1ADDQ BX, AXRETTEXT main.main(SB), NOSPLIT|NOFRAME|ABIInternal, $0-0FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)RET
从上述的代码中,我们可以清晰的看到,出现了并非X86/X64汇编语法的,FUNCDATA 、PCDATA 两个指令。
它们是 GO 汇编之中的伪指令,注意它是伪指令,意思就是说这东西不能用,除了GO的编译器能理解它之外,其它的汇编器,无论 GCC、VC++ 都是不认识这个东西。
人们可以理解,Go 存在两个编译过程,一个前端编译器,一个后端编译器,前端编译器就是把我们写的 .go 源文件的程序代码编译为 Go 后端编译器认识的 Go 汇编指令集代码。
这的确很类似于 JAVA/JVM 编译的字节码、C# 编译器的 MSIL 中间指令代码,但又存在明显的区别,人们可以显著的参考下述在ARM平台输出的 Go 汇编代码
TEXT main.Add(SB), LEAF|NOFRAME|ABIInternal, $-4-12FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)FUNCDATA $5, main.Add.arginfo1(SB)MOVW main.x(FP), R0MOVW main.y+4(FP), R1ADD R1, R0, R0MOVW R0, main.~r0+8(FP)JMP (R14)TEXT main.main(SB), LEAF|NOFRAME|ABIInternal, $-4-0FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)JMP (R14)
人们可以明显的看到,除了几个伪指令是相同他的,但是内部实现所使用的指令发生了变化,这是因为,Go 每个平台编译器生成的 Go 汇编代码会根据CPU指令集平台的不同而不同,这是因为 Go 虽然编译的是只能给 Go 后端编译器看的汇编代码。
但不意味着它会完全按照先编译为字节码、中间代码的形式,Go 前端编译器输出的 Go 汇编,在编译的过程中,就已经按照目的平台的指令集进行了一部分的翻译(不完全是真汇编,但汇编已很接近了。)
剩下那部分伪指令是让 Go 汇编器,在构建目的程序时,所需处理的东西,就是GC、外挂栈空间内存上面的参数、局部变量读取这些实现,最后生成的目的汇编代码,才是用来编译为目的PE、ELF可执行文件的。
OK:这里简单的描述下上面X86汇编的意义,ARM我不怎么看得懂,所以不在此处献丑了
第一句 Go 汇编指令:
TEXT main.Add(SB), NOSPLIT|NOFRAME|ABIInternal, $0-16
1、TEXT: 这是一个伪指令,用于指示下面的代码是函数代码(类似于其他汇编语言中的函数标签)。
2、main.Add(SB): main.Add 是函数的名称,SB 表示 Static Base(静态基址),它是一个汇编符号,指示函数相对于全局数据区的偏移量。
3、NOSPLIT|NOFRAME|ABIInternal: 这是函数的属性标志。NOSPLIT 指示编译器不应在函数内插入栈分裂代码,NOFRAME 指示编译器不应创建函数堆栈帧,ABIInternal 表示该函数的调用约定为 Go 内部使用。
4、$0-16: 这是函数的栈帧大小指令。$0 表示该函数不会在栈上分配任何局部变量的空间,-16 表示函数会从参数中读取16字节的数据。
注意:这个栈空间指的是 Go 程序外挂的栈哈,不是进程线程的栈空间。(或为虚拟栈空间)
第二句 Go 汇编指令:
FUNCDATA $0, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
1、这是一个 FUNCDATA 伪指令,用于插入与垃圾回收(garbage collection)相关的元数据。
2、$0 表示这段元数据的索引值为 0(参数位:0 = X)
3、gclocals·g2BeySu+wFnoycgXfElmcg==(SB) 是一个符号名,它引用了一个包含局部变量和参数信息的数据结构。
第三句 Go 汇编指令:
FUNCDATA $1, gclocals·g2BeySu+wFnoycgXfElmcg==(SB)
跟第二句没区别,元数据索引值为 1(参数位:1 = Y)
第四句 Go 汇编指令:
FUNCDATA $5, main.Add.arginfo1(SB)
main.Add.arginfo1(SB) 是获取 “描述函数参数类型和数量的数据结构的引用地址”。
Go 语言没有显示的函数签名声明,所以编译器需要这个函数的参数信息,以便于可以正确的传递参数值给该函数。
第五句 Go 汇编指令:
FUNCDATA $6, main.Add.argliveinfo(SB)
main.Add.argliveinfo(SB) 是获取 “描述函数参数活跃性的数据结构的引用地址”
参数的活跃性指的是在函数执行期间哪些参数被使用了。这些信息对于优化代码的执行效率非常重要,GO GC在用。
第六句 Go 汇编指令
PCDATA $3, $1
把 $1 的值复制到 $3,AT&T汇编风格是:
操作数 原操作数, 目标操作数
加法实现 GO 汇编指令
ADDQ BX, AX
RET
1、AX 和 BX 寄存器用于存储 x 和 y 的值。
2、之后,通过 ADDQ BX, AX 指令将 y 的值加到 x 上,并将结果保存在 AX 寄存器中。
3、最后,使用 RET 指令将结果返回。
总结:
1、Golang 协程不会保存CPU寄存器的值。
2、Golang 协程属于 Stackless 协程的一种变种。
3、Golang 通过为外挂计算栈内存空间,来实现类似有栈协程的效果。
4、Golang 两个协程可能在不同的物理线程上面工作,所以公用数据访问时,须注意同步问题。
5、Golang 协程在处理异步操作的时,让出了当前协程占用的线程CPU,协程处于WAIT状态时, 当前协程依赖的外部数据,可能在外部发生了改变或者释放。
所以,该协程被唤醒之后(resume\awake)理应检查当前依赖数据的状态,如:在该协程处于 Yield 等待状态之中时,其它协程调用了 Dispose 函数,释放了 “它(公用数据)” 持有的全部被托管及非托管资源。
6、Golang 也会适用寄存器优化,但这有一些前提,就是简单的算术运算,可以被编译为寄存器优化的代码,这不冲突,只是最终会把值存储到 “Go” 为每个协程分配的外挂栈内存空间上面。
就像在 MSIL 之中,人们执行 stloc.s、ldloc.s、ldarg.s、starg.s 这些指令集一样,只不过它不像微软的 .NET CLR 会把这些代码编译为近似 C/C++ 编译器输出的目标平台汇编代码,当然不管怎么做,这类由GC系统标记的语言,都会在最终编译输出的汇编代码之中插入引用技术管理的实现,区别是在什么地方插入,当然这的看GC系统是怎么设计的,比如链式遍历的GC,就不需要在每个函数引用资源的地方去做 AddRef、到结尾做 ReleaseRef 这样的行为,但缺点就是GC在处理终结的时候,CPU开销比较大。
7、Golang 之中托管资源是通过RID间接引用的,即托管资源并非是直接使用指针,这是因为资源或会被GC压缩或移动碎片整理,当然这个时候会导致阻塞问题,即:GC Pinned 问题。
相关文章:
关于 Go 协同程序(Coroutines 协程)、Go 汇编及一些注意事项。
参考: Go 汇编函数 - Go 语言高级编程 Go 嵌套汇编 - 掘金 (juejin.cn) 前言: Golang 适用 Go-Runtime(Go 运行时,嵌入在被编译的PE可执行文件之中)来管理调度协同程式的运行。 Go 语言没有多线程(MT&a…...
深入剖析BaseMapperPlus扩展接口及其在MyBatis-Plus中的实践价值
前言 BaseMapperPlus并非MyBatis-Plus(MP)官方提供的标准接口,而是社区开发者基于MP的BaseMapper接口进行二次封装和增强后创建的一个自定义接口。这个概念可能因不同项目或个人实践而有所差异,但其核心思想是为了解决特定场景下…...

Linux之安装配置VCentOS7+换源
目录 一、安装 二、配置 三、安装工具XSHELL 3.1 使用XSHELL连接Linux 四、换源 前言 首先需要安装VMware虚拟机,在虚拟机里进行安装Linux 简介 Linux,一般指GNU/Linux(单独的Linux内核并不可直接使用,一般搭配GNU套件&#…...

[极客大挑战 2019]LoveSQL1
万能密码测试,发现注入点 注意这里#要使用url编码才能正常注入 测试列数,得三列 查看table,一个是geekuser另一个是l0ve1ysq1 查看column,有id,username,password,全部打印出来,…...
网络安全的介绍
1.什么是网络安全 网络安全是一门关注保护计算机系统、网络基础设施和数据免受未经授权访问、破坏或窃取的学科。随着数字化时代的发展,网络安全变得尤为重要,因为大量的个人信息、商业机密和政府数据都储存在电子设备和云端系统中。以下是网络安全的概…...

django邮件通知功能-
需求: 1:下单人员下订单时需要向组长和投流手发送邮件通知 2:为何使用邮件通知功能?因为没钱去开通短信通知功能 设计 1:给用户信息表添加2个字段 第一个字段为:是否开通邮件通知的布尔值 第二个字段为: 用…...

C++ 类定义
C 类定义 定义一个类需要使用关键字 class,然后指定类的名称,并类的主体是包含在一对花括号中,主体包含类的成员变量和成员函数。 定义一个类,本质上是定义一个数据类型的蓝图,它定义了类的对象包括了什么࿰…...

IntelliJ IDE 插件开发 | (五)VFS 与编辑器
系列文章 IntelliJ IDE 插件开发 |(一)快速入门IntelliJ IDE 插件开发 |(二)UI 界面与数据持久化IntelliJ IDE 插件开发 |(三)消息通知与事件监听IntelliJ IDE 插件开发 |(四)来查收…...

金融OCR领域实习日志(一)
一、OCR基础 任务要求: 工作原理 OCR(Optical Character Recognition,光学字符识别)是指电子设备(例如扫描仪或数码相)检查纸上打印的字符,经过检测暗、亮的模式肯定其形状,而后用…...
CC++编译和链接介绍
介绍 C语言的编译和链接是将源代码转换为可执行文件的两个关键步骤。以下是详细的流程: 编译过程(Compilation) 预处理(Preprocessing): 编译器首先对源代码进行预处理,这个阶段处理#include包…...
Element-UI中的el-upload插件上传文件action和headers参数
官网给的例子action都是绝对地址,我现在需要上传到自己后台的地址,只有一个路由地址/task/upload 根据 config/index.js配置,那么action要写成/api/task/upload,另外也可以传入函数来返回地址:action"uploadUrl()"。 …...

在IntelliJ IDEA中通过Spring Boot集成达梦数据库:从入门到精通
目录 博客前言 一.创建springboot项目 新建项目 选择创建类型编辑 测试 二.集成达梦数据库 添加达梦数据库部分依赖 添加数据库驱动包 配置数据库连接信息 编写测试代码 验证连接是否成功 博客前言 随着数字化时代的到来,数据库在应用程序中的地位越来…...

docker相关
下载Ubuntu18.04文件64位(32位安装不了MySQL) https://old-releases.ubuntu.com/releases/18.04.4/?_ga2.44113060.1243545826.1617173008-2055924693.1608557140 Linux ubuntu16.04打开控制台:到桌面,可以按快捷键ctrlaltt 查…...

生产力工具|卸载并重装Anaconda3
一、Anaconda3卸载 (一)官方方案一(Uninstall-Anaconda3-不能删除配置文件) 官方推荐的方案是两种,一种是直接在Anaconda的安装路径下,双击: (可以在搜索栏或者使用everything里面搜…...

大模型学习与实践笔记(十二)
使用RAG方式,构建opencv专业资料构建专业知识库,并搭建专业问答助手,并将模型部署到openxlab 平台 代码仓库:https://github.com/AllYoung/LLM4opencv 1:创建代码仓库 在 GitHub 中创建存放应用代码的仓库ÿ…...

Vulnhub靶机:FunBox 5
一、介绍 运行环境:Virtualbox 攻击机:kali(10.0.2.15) 靶机:FunBox 5(10.0.2.30) 目标:获取靶机root权限和flag 靶机下载地址:https://www.vulnhub.com/entry/funb…...

性能优化(CPU优化技术)-NEON指令介绍
「发表于知乎专栏《移动端算法优化》」 本文主要介绍了 NEON 指令相关的知识,首先通过讲解 arm 指令集的分类,NEON寄存器的类型,树立基本概念。然后进一步梳理了 NEON 汇编以及 intrinsics 指令的格式。最后结合指令的分类,使用例…...

【极数系列】Flink环境搭建(02)
【极数系列】Flink环境搭建(02) 引言 1.linux 直接在linux上使用jdk11flink1.18.0版本部署 2.docker 使用容器部署比较方便,一键启动停止,方便参数调整 3.windows 搭建Flink 1.18.0版本需要使用Cygwin或wsl工具模拟unix环境…...

仓储管理系统——软件工程报告(需求分析)②
需求分析 一、系统概况 仓库管理系统是一种基于互联网对实际仓库的管理平台,旨在提供一个方便、快捷、安全的存取货物和查询商品信息平台。该系统通过在线用户登录查询,可以线上操作线下具体出/入库操作、查询仓库商品信息、提高仓库运作效率ÿ…...

立创EDA学习:PCB布局
参考内容 【PCB布线教程 | 嘉立创EDA专业版入门教程(11)】 https://www.bilibili.com/video/BV1mW4y1Z7kb/?share_sourcecopy_web&vd_sourcebe33b1553b08cc7b94afdd6c8a50dc5a 单路布线 遵循顺序 先近后远,先易后难 可以拖动让拐角缩小…...

Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...

简易版抽奖活动的设计技术方案
1.前言 本技术方案旨在设计一套完整且可靠的抽奖活动逻辑,确保抽奖活动能够公平、公正、公开地进行,同时满足高并发访问、数据安全存储与高效处理等需求,为用户提供流畅的抽奖体验,助力业务顺利开展。本方案将涵盖抽奖活动的整体架构设计、核心流程逻辑、关键功能实现以及…...
1688商品列表API与其他数据源的对接思路
将1688商品列表API与其他数据源对接时,需结合业务场景设计数据流转链路,重点关注数据格式兼容性、接口调用频率控制及数据一致性维护。以下是具体对接思路及关键技术点: 一、核心对接场景与目标 商品数据同步 场景:将1688商品信息…...
系统设计 --- MongoDB亿级数据查询优化策略
系统设计 --- MongoDB亿级数据查询分表策略 背景Solution --- 分表 背景 使用audit log实现Audi Trail功能 Audit Trail范围: 六个月数据量: 每秒5-7条audi log,共计7千万 – 1亿条数据需要实现全文检索按照时间倒序因为license问题,不能使用ELK只能使用…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...

视觉slam十四讲实践部分记录——ch2、ch3
ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...

AirSim/Cosys-AirSim 游戏开发(四)外部固定位置监控相机
这个博客介绍了如何通过 settings.json 文件添加一个无人机外的 固定位置监控相机,因为在使用过程中发现 Airsim 对外部监控相机的描述模糊,而 Cosys-Airsim 在官方文档中没有提供外部监控相机设置,最后在源码示例中找到了,所以感…...

uniapp手机号一键登录保姆级教程(包含前端和后端)
目录 前置条件创建uniapp项目并关联uniClound云空间开启一键登录模块并开通一键登录服务编写云函数并上传部署获取手机号流程(第一种) 前端直接调用云函数获取手机号(第三种)后台调用云函数获取手机号 错误码常见问题 前置条件 手机安装有sim卡手机开启…...
探索Selenium:自动化测试的神奇钥匙
目录 一、Selenium 是什么1.1 定义与概念1.2 发展历程1.3 功能概述 二、Selenium 工作原理剖析2.1 架构组成2.2 工作流程2.3 通信机制 三、Selenium 的优势3.1 跨浏览器与平台支持3.2 丰富的语言支持3.3 强大的社区支持 四、Selenium 的应用场景4.1 Web 应用自动化测试4.2 数据…...

解析两阶段提交与三阶段提交的核心差异及MySQL实现方案
引言 在分布式系统的事务处理中,如何保障跨节点数据操作的一致性始终是核心挑战。经典的两阶段提交协议(2PC)通过准备阶段与提交阶段的协调机制,以同步决策模式确保事务原子性。其改进版本三阶段提交协议(3PC…...