1.说说Golang的runtime
2.go runtime schedule 原理
3.万字长文深入浅出 Golang Runtime
4.彻底解决Golang获取当前项目绝对路径问题
5.Golang源码剖析panic与recover,看不懂你打我好了
6.Go语言Runtime运行时-知多少?
说说Golang的runtime
Go语言的runtime模块是核心组件,负责与操作系统进行交互,以及对goroutine(轻量级线程)的调度和垃圾回收管理。它包括了debugging工具如pprof和tracer,用于问题排查和性能分析,dicomgo源码以及处理异常事件,如goroutine的创建和销毁、系统调用等。此外,runtime还特别设计了一个独立的调度器,以解决与操作系统内核调度器的差异,如避免过多的上下文切换和GC时的“stop-the-world”问题。
调度器是运行时的关键,尽管操作系统内核已经负责线程调度,Go语言却有自己的scheduler。这源于Go语言的并发特性,它直接实现了语言级别上的并发,而非依赖于操作系统。Go的scheduler负责管理goroutine的执行,确保在垃圾回收期间,只暂停当前在CPU上执行的goroutine,而不是整个程序,从而提供更高的效率和控制。
调度器的实现涉及到P、M和G的概念。P代表一个进程,M代表一个工作线程,而G则是goroutine的实例。程序启动时,runtime会创建一个系统线程执行sysmon,监控goroutine状态并进行垃圾回收。当一个P连续执行时间过长时,会通过preemptive调度策略,将控制权从这个G转移到其他等待的G,确保资源的有效利用。
关于垃圾回收,Go的优化历程显著,从早期的毫秒到现在的微秒级,特别是1.8版本引入了goroutine级别的GC,进一步提升了性能和效率。未来,显示涨停主图源码Go将继续在吞吐量和低延迟之间寻求平衡,为开发者提供更强大的并发支持。
详细了解Go运行时的内部工作,可以参考相关博客和文档,如CSDN和知乎上的资源。
go runtime schedule 原理
在Golang中,程序的执行并非从main包的main函数开始,而是通过runtime·rt0_go这个关键函数。这个函数在程序启动后被调用,负责程序的初始化和调度系统的启动。 rt0_go的执行流程如下:首先,通过runtime·osinit获取系统CPU数量。
然后,调用runtime·schedinit初始化调度系统,包括p(进程)的设置和m0(管理器)与某个p的绑定。
接着,创建一个主goroutine(runtime·newproc),其任务函数为runtime.main,被放入m0绑定的p的本地队列。
最后,调用runtime·mstart启动管理器m,进入调度系统。
具体涉及的函数和数据结构包括:os_linux.go, proc.go, mstart, findRunnable, runqget, runtime.main等。 对于主goroutine的创建,newproc函数在runtime/proc.go中扮演重要角色,而newproc1函数则负责将其放入m0的p的本地队列。 调度系统的工作原理是通过schedule函数找到可运行的goroutine并执行,findRunnable和runqget函数参与了这一过程。 main函数在runtime/proc.go中定义,而g(goroutine)、p(进程)和m(管理器)之间的关系密切,它们在数据结构上相互关联,g通过m找到对应的p,反之亦然。 深入了解Golang运行时调度,可以参考深入golang runtime的调度和图解Go运行时调度器的文档。万字长文深入浅出 Golang Runtime
深入理解 Go 语言的 Runtime:关键组件与工作原理
探索 Go 语言 Runtime 的世界,它如何与用户代码无缝协作?从 Go SDK 的 runtime 目录开始,这个部分主要关注四个核心功能。
Go 的 Runtime 与 Java、Python 的烟台溯源码即食燕窝虚拟机有所不同。Go 将 runtime 和用户代码集成,一起编译成可执行文件,用户代码与 runtime 的界限仅限于代码结构,而非运行时隔离。关键字被编译为 runtime 包下的函数调用,体现其紧密整合。
Runtime 的版本历史中,关键更新点和垃圾收集器(GC)的 STW(Stop The World)提供了关键信息。随着语言的发展,调度机制经历了从进程到线程,再到协程的演变,以减少切换成本和提高效率。
理解调度,首先要理解运行和阻塞。在 Go 的协程中,这两者可能与直觉不同。实际上,CPU 像处理事务的主体,而非线程或协程。Go 的协程更像是 CPU 中的执行流,通过上下文切换实现并发执行,而非阻塞线程。
Go 的协程结构体 runtime.g 代表了执行流,包含了函数指针、参数和状态。协程切换和恢复通过特定的汇编函数实现,确保了非阻塞的网络操作和高效资源管理。
从 Go1.0 到 GPM 模型,Go 的内存管理策略发生了变化,通过 P 值和 mcache 调整了并发执行的并行度,以最大化 CPU 利用率。Go 的协程状态转换反映出其与线程类似但并非实体的特性。
内存分配方面,Go 采用类似 TCMalloc 的方法,利用连续内存页减少碎片。1. 版本引入的稀疏索引管理解决了内存扩展和碎片问题,支持更大内存容量。
span 机制是内存分配的关键,它管理着 8KB 的内存块,确保内存分配的效率。多层次的java源码宠物合成类缓存系统,如 mcache 和 mcentral,以及特殊的分配器 fixalloc,共同构建了内存分配的复杂网络。
GC 是 Go 中不可或缺的一部分,通过三色标记清除法确保正确性。写屏障机制解决了并发标记中的问题,而 Golang 的 GC Pacer 则确保了并发 GC 的同步与性能平衡。
实践中,观察调度和 GC 有助于理解 Go 的性能和资源管理。通过工具如 pprof 和 gops,可以更直观地分析和优化运行时性能。
总结来说,Go 的 Runtime 是一个并行、层次分明的系统,通过精心设计的内存管理和高效的调度机制,实现了并发编程的高效率和灵活性。理解这些核心组件是深入掌握 Go 编程的关键。
彻底解决Golang获取当前项目绝对路径问题
由于Golang是编译型语言,获取当前执行目录变得复杂。传统做法是通过启动传参或环境变量手动传递路径,但今天发现了一种更便捷的解决方案。
Go程序有两种执行方式:go run和go build。这两种方式在获取当前执行路径时会产生不同的问题。
下面直接展示代码示例。我们编写一个获取当前可执行文件路径的方法,然后通过go run和go build两种方式来测试。
通过对比执行结果,我们发现go run获取到的路径是错误的。原因是go run会将源代码编译到系统TEMP或TMP环境变量目录中并启动执行,而go build只会在当前目录编译出可执行文件,并不会自动执行。
我们可以简单理解为,go run main.go等价于go build & ./main。虽然两种执行方式最终都是一样的过程,但他们的执行目录却完全不一样了。
在我查看服务日志(zap库)时,发现了一种新的解决方案。比如一条简单的日志,服务是通过go run启动的,但日志库却正确地打印出了程序路径D:/Projects/te-server/modules/es/es.go:。
我发现这是影视解析源码怎么使用通过runtime.Caller()实现的,而所有Golang日志库都会有runtime.Caller()这个调用。我以为找到了最终答案,然后写代码试了下,结果完全正确!但后来发现,在Linux上运行时,它会打印出Windows的路径,这让我很失望。
我意识到,既然go run时可以通过runtime.Caller()获取到正确的结果,go build时也可以通过os.Executable()来获取到正确的路径;那如果我能判定当前程序是通过go run还是go build执行的,选择不同的路径获取方法,所有问题不就迎刃而解了吗。
Go没有提供接口来区分程序是go run还是go build执行,但我们可以根据go run的执行原理来判断。我们可以直接在程序中对比os.Executable()获取到的路径是否与环境变量TEMP设置的路径相同,如果相同,说明是通过go run启动的,因为当前执行路径是在TEMP目录;不同的话自然是go build的启动方式。
下面是完整代码:
在windows执行
在windows编译后上传到Linux执行
对比结果,我们可以看到,在不同的系统中,不同的执行方式,我们封装的getCurrentAbPath方法最终都输出的正确的结果,perfect!
Golang源码剖析panic与recover,看不懂你打我好了
哈喽,大家好,我是asong,今天与大家来聊一聊go语言中的"throw、try.....catch{ }"。如果你之前是一名java程序员,我相信你一定吐槽过go语言错误处理方式,但是这篇文章不是来讨论好坏的,我们本文的重点是带着大家看一看panic与recover是如何实现的。上一文我们讲解了defer是如何实现的,但是没有讲解与defer紧密相连的recover,想搞懂panic与recover的实现也没那么简单,就放到这一篇来讲解了。废话不多说,直接开整。
Go 语言中panic 关键字主要用于主动抛出异常,类似 java 等语言中的 throw 关键字。panic 能够改变程序的控制流,调用 panic 后会立刻停止执行当前函数的剩余代码,并在当前 Goroutine 中递归执行调用方的 defer;
Go 语言中recover 关键字主要用于捕获异常,让程序回到正常状态,类似 java 等语言中的 try ... catch 。recover 可以中止 panic 造成的程序崩溃。它是一个只能在 defer 中发挥作用的函数,在其他作用域中调用不会发挥作用;
recover只能在defer中使用这个在标准库的注释中已经写明白了,我们可以看一下:
这里有一个要注意的点就是recover必须要要在defer函数中使用,否则无法阻止panic。最好的验证方法是先写两个例子:
运行我们会发现example2()方法的panic是没有被recover住的,导致整个程序直接crash了。这里大家肯定会有疑问,为什么直接写recover()就不能阻止panic了呢。我们在 详解defer实现机制(附上三道面试题,我不信你们都能做对)讲解了defer实现原理,一个重要的知识点**defer将语句放入到栈中时,也会将相关的值拷贝同时入栈。**所以defer recover()这种写法在放入defer栈中时就已经被执行过了,panic是发生在之后,所以根本无法阻止住panic。
通过运行结果可以看出panic不会影响defer函数的使用,所以他是安全的。
这里我开了两个协程,一个协程会发生panic,导致程序崩溃,但是只会执行自己所在Goroutine的延迟函数,所以正好验证了多个 Goroutine 之间没有太多的关联,一个 Goroutine 在 panic 时也不应该执行其他 Goroutine 的延迟函数。
其实我们在实际项目开发中,经常会遇到panic问题, Go 的 runtime 代码中很多地方都调用了 panic 函数,对于不了解 Go 底层实现的新人来说,这无疑是挖了一堆深坑。我们在实际生产环境中总会出现panic,但是我们的程序仍能正常运行,这是因为我们的框架已经做了recover,他已经为我们兜住底,比如gin,我们看一看他是怎么做的。
我们先来写个简单的代码,看看他的汇编调用:执行go tool compile -N -l -S main.go就可以看到对应的汇编码了,我们截取部分片段分析:
上面重点部分就是画红线的三处,第一步调用runtime.deferprocStack创建defer对象,这一步大家可能会有疑惑,我上一文忘记讲个这个了,这里先简单概括一下,defer总共有三种模型,编译一个函数里只会有一种defer模式。在讲defer实现机制时,我们一起看过defer的结构,其中有一个字段就是_panic,是触发defer的作用,我们来看看的panic的结构:
简单介绍一下上面的字段:
上面的pc、sp、goexit我们单独讲一下,runtime包中有一个Goexit方法,Goext能够终止调用它的goroutine,其他的goroutine是不受影响的,goexit也会在终止goroutine之前运行所有延迟调用函数,Goexit不是一个panic,所以这些延迟函数中的任何recover调用都将返回nil。如果我们在主函数中调用了Goexit会终止该goroutine但不会返回func main。由于func main没有返回,因此程序将继续执行其他gorountine,直到所有其他goroutine退出,程序才会crash。
下面就开始我们的重点吧~。
在讲defer实现机制时,我们一起看过defer的结构,其中有一个字段就是_panic,是触发defer的作用,我们来看看的panic的结构:简单介绍一下上面的字段:上面的pc、sp、goexit我们单独讲一下,runtime包中有一个Goexit方法,Goext能够终止调用它的goroutine,其他的goroutine是不受影响的,goexit也会在终止goroutine之前运行所有延迟调用函数,Goexit不是一个panic,所以这些延迟函数中的任何recover调用都将返回nil。如果我们在主函数中调用了Goexit会终止该goroutine但不会返回func main。由于func main没有返回,因此程序将继续执行其他gorountine,直到所有其他goroutine退出,程序才会crash。写个简单的例子:运行上面的例子你就会发现,即使在主goroutine中调用了runtime.Goexit,其他goroutine是没有任何影响的。所以结构中的pc、sp、goexit三个字段都是为了修复runtime.Goexit,这三个字段就是为了保证该函数的一定会生效,因为如果在defer中发生panic,那么goexit函数就会被取消,所以才有了这三个字段做保护。看这个例子:
英语好的可以看一看这个: github.com/golang/go/is...,这就是上面的一个例子,这里就不过多解释了,了解就好。
接下来我们再来看一看gopanic方法。
gopanic的代码有点长,我们一点一点来分析:
根据不同的类型判断当前发生panic错误,这里没什么多说的,接着往下看。
上面的代码都是截段,这些部分都是为了判断当前defer是否可以使用开发编码模式,具体怎么操作的就不展开了。
在第三部分进行defer内联优化选择时会执行调用延迟函数(reflectcall就是这个作用),也就是会调用runtime.gorecover把recoverd = true,具体这个函数的操作留在下面讲,因为runtime.gorecover函数并不包含恢复程序的逻辑,程序的恢复是在gopanic中执行的。先看一下代码:
这段代码有点长,主要就是分为两部分:
第一部分主要是这个判断if gp._panic != nil && gp._panic.goexit && gp._panic.aborted { ... },正常recover是会绕过Goexit的,所以为了解决这个,添加了这个判断,这样就可以保证Goexit也会被recover住,这里是通过从runtime._panic中取出了程序计数器pc和栈指针sp并且调用runtime.recovery函数触发goroutine的调度,调度之前会准备好 sp、pc 以及函数的返回值。
第二部分主要是做panic的recover,这也与上面的流程基本差不多,他是从runtime._defer中取出了程序计数器pc和栈指针sp并调用recovery函数触发Goroutine,跳转到recovery函数是通过runtime.call进行的,我们看一下其源码(src/runtime/asm_amd.s 行):
因为go语言中的runtime环境是有自己的堆栈和goroutine,recovery函数也是在runtime环境执行的,所以要调度到m->g0来执行recovery函数,我们在看一下recovery函数:
在recovery 函数中,利用 g 中的两个状态码回溯栈指针 sp 并恢复程序计数器 pc 到调度器中,并调用 gogo 重新调度 g , goroutine 继续执行,recovery在调度过程中会将函数的返回值设置为1。这个有什么作用呢? 在deferproc函数中找到了答案:
当延迟函数中recover了一个panic时,就会返回1,当 runtime.deferproc 函数的返回值是 1 时,编译器生成的代码会直接跳转到调用方函数返回之前并执行 runtime.deferreturn,跳转到runtime.deferturn函数之后,程序就已经从panic恢复了正常的逻辑。
在这里runtime.fatalpanic实现了无法被恢复的程序崩溃,它在中止程序之前会通过 runtime.printpanics 打印出全部的 panic 消息以及调用时传入的参数。
这就是这个逻辑流程,累死我了。。。。
结尾给大家发一个小福利,哈哈,这个福利就是如果避免出现panic,要注意这些:这几个是比较典型的,还有很多会发生panic的地方,交给你们自行学习吧~。
好啦,这篇文章就到这里啦,素质三连(分享、点赞、在看)都是笔者持续创作更多优质内容的动力!
Go语言Runtime运行时-知多少?
理解Go语言的运行时(runtime)是深入掌握Go开发的关键步骤。runtime并非解释器也不是完整运行时环境,例如Java虚拟机,而是一个为开发者提供强大工具的Go库,位于golang.org/pkg/runtime/。它对于程序执行期间的状况分析和观察具有重要作用。
简而言之,runtime是Go程序执行时依赖的库,其功能涉及垃圾回收、内存管理、goroutine调度等核心内容。对这些机制有深入理解,将极大地提升程序性能和稳定性。
深入研究runtime,可以探索其在内存管理和垃圾回收的运作机制,包括如何高效管理内存和避免内存泄漏。此外,了解goroutine调度,即MPG的实现,对于优化并发性能至关重要。
runtime提供了丰富的函数和API,帮助开发者进行程序分析、性能提升和测试等。例如,用于垃圾回收和内存管理的函数,以及用于管理goroutine调度的工具。
然而,尽管runtime提供了强大的功能,但在实际的程序开发中,通常避免直接调用这些函数,以确保程序的简洁性和可维护性。正确使用runtime,能显著提升程序性能和效率。
理解runtime是成为高效Go开发者的必经之路。通过深入学习和实践,你可以更有效地利用runtime提供的工具,优化你的程序,实现更高的性能和更好的资源管理。