几个关于 Golang Go语言 Runtime 的问题
几个关于 Golang Go语言 Runtime 的问题
问题:go 的 runtime 如何实现?(这个实在不理解)
追问:runtime 这个进程是运行在哪儿的?
追问:每个 go 进程都会有一个吗还是共用一个?(应该是共用一个)
追问:如果你运行了 go 进程,在 linux 系统里查看进程能看到 runtime 进程吗?
golang 的 runtime 源码在 src 可以直接找到。真正的 main 函数其实是在 runtime 里。然后你可以看到启动的步骤,包括不限于启动 g0 协程和启动 gc 工作。
每个 go 进程是自己独立的,和虚拟机区分开来。没有独立的 runtime 进程。
简单的讲,你可以理解成 c 语言加入了协程和 gc 的框架。由于有 gc,所以不能直接使用系统分配内存的函数,都是通过这个框架提供的函数来分配。
更多关于几个关于 Golang Go语言 Runtime 的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
runtime 就是你编译出的程序中你自己的代码以外的部分。
比如一个 c 程序,不会直接执行 main,而是执行_start (多数平台),这个函数会先进行一些栈空间等初始化,然后再调用你的 main 函数。这个函数就是由 c 的 runtime 提供。
go 这样的语言 runtime 做的事情更多,初始化线程池、调度器、gc 、unwind 等,然后再把你的 main 函数作为一个 goroutine 启动。
入口代码在 src/runtime/proc.go 。有个 func main()
别的不说,如果这是面试,如果你都回复不知道 go runtime 怎么实现了,还追着问下面的三个问题,那这面试官妥妥 sb 。
runtime 只是一个名字而已,本质只是一个“库”,go 程序编译时它将和我们编写的代码一同编译为一个可执行文件
golang.org/doc/faq#runtime
C 也有 runtime 呢,应该去想想 crt 怎么做的(别说人家叫 libc 就不算 crt 了)
C++也有 runtime,叫 libstdc++/libc++/vcruntime
C 的可以共享,也可以静态链接进去(
go 的就不行了,每个都自己带一份
golang 的 runtime 其实是个线程池,用 work-steal 调度算法调度 goroutine 。golang 的实现已知的有 go gccgo llgo,gccgo 也是 go 的官方实现就是下面的 gofrontend,llgo 已经完了。参见 https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit#heading=h.nfb3viti0vlt
runtime 运行在 OS 线程上,比如 pthread 。参见 https://github.com/golang/gofrontend/blob/master/libgo/runtime/go-libmain.c#L208
go 进程应该是 goroutine 吧。这个真不好回答。有很多个,但至少有一个 runtime.main,至于最多有多少个,如果 golang 实现者考虑过这个问题的话,会有一个可以设置的参数的,可以用环境变量 GOMAXPROCS 限制 OS 线程数。
比如:一个只有 http.ListenAndServe 的 pprof go 程序,有 3 个 goroutine,5 个 OS 线程。//go version go1.15.7 darwin/amd64
linux 里系统查看 go 进程是指 top 用查看 go 程序吧。top 查看到的是那个“线程池”,用 pprof 可以查看 goroutine 。
参见 https://golang.google.cn/pkg/net/http/pprof/
runtime 不是完全独立于你程序的一个进程。相反,你写的程序是作为 goroutine 在 runtime 中的调度器中运行。当然 runtime 也不只有调度器,还有其他 GC 什么的程序在运行。
https://talkgo.org/t/topic/31 讲解 golang 的协程调度实现
#7 我补充一点吧
go runtime,严格来讲就是以 golang 下 以 goroutine 结合管道通信的并发模型,这个并发模型是基于 stackfull 的协程模型,相反的是 stackless 的协程模型,而传统其它语言可能是基于线程池以及锁的并发模型 ,有兴趣可以看 stackfull 跟 stackless 的区别,前者会占用空间,后者对内存空间友好,但是现在是 2021 年了,谁家老板买不起 2T 内存? golang 选的是 stackfull 协程模型,当然这里比较简单,下面会详细介绍,有兴趣深入还是根据我的关键字去找书看。
然后 stackfull 的本质就是 每个协程有自己的栈幁,具体我没有关注过 goroutine 的栈幁结构,但是从我观察 goroutine 进入内核态的汇编代码,做了一次 go 的调用约定 到 amd64 fastcall 的调用约定转换,基本上可以确认 golang 内部的函数调用约定以及栈幁结构 应该是平台无关化的,另外 golang 貌似是不支持二进制 lib 编译的,是不是因为内部大版本之间的 ABI 是否从来未稳定过。
然后弄这个 stackfull 的协程模型是因为 golang 这个语言的野心很大,它希望能把操作系统内核态任务调度这个事情全权交给应用态的 go 调度系统来作,而之前所有的语言 包括 C++ Java C 都是将调度模型交给操作系统提供的线程抽象,这一点上 golang 是一个伟大的进步 配合管道跟非阻塞式 IO 以及 epoll 调用,可以在用户态实现一个无锁且按需调度的调度系统,可以说 go 它是专门为后端服务设计的,当然从语法上来看,目前不支持泛型,我暂时没有加大对 go 这门语言的投入,但是它这个设计理念是好的,那就是带有运行时的语言应该从操作系统那里拿回原本属于我们应用开发人员的调度功能。
这里可以提出两点
1. 管道模型为什么优于锁 monitor ? 传统操作系统内核提供的监视器锁 存在惊群的问题,这是很难避免的,因为操作系统并不知道你要唤醒多少个线程,当然你也可以指定唤醒的数量,这里就要做到很精细化的锁唤醒操作,例如 Java 里面的 blockingQueue 当队列满了的时候 操作的线程会去 唤醒因为读等待的线程而不是去唤醒因为写等待的线程,当队列空了的时候,当前操作线程就会去唤醒因为写等待的线程而不是去唤醒因为读等待的线程,而之前我所说的这些操作,都需要对并发编程有很高的理解,你才能设计出一个多线程并发且线程安全的队列,这一点对开发人员要求很高,而且从因为监视器锁 Java 的线程会频繁进入内核 开销很大。
但如果使用管道,其实从 golang 调度器来看,当你想从一个管道读数据而挂起的时候,其实 golang 调度器只要把当前线程的几个寄存器换掉就能把对 CPU 的控制权转移到另外一个协程上,这中间无需进入内核调度,而且 golang 还可以做一些优化,把对 CPU 的控制器优先配给需要写这个管道的协程,或者把对同一个管道读写的协程都分配到一个 CPU 上,这样一来 golang 使用管道可以实现完全无锁化的协程之间的通信,但是从编写 golang 的协程代码的人来看,他的大脑负担就会少很多。
第二点 我待会下午补充
对于 C10K 问题
谢谢以上大佬们的回复
听不懂 有点难了
#14 没事,多搜索就好了…
我分享的 可能广度 深度都有,
可能很多人 根本不知道 ABI monitor 监视器 栈幁 fastcall stackfuul stackless 这种东西,
你如果计算机体系结构 都了解的话,就不会产生这种疑惑了
建议大佬整理一下单独成文👍🏿
我之前通过一段时间的学习,也觉得 go 的这种自建调度策略的做法非常优雅,但是再让我说为什么好就说不出了,最近也打算再深入一下
#16 其实也不是优雅的问题,以前线程的通信,肯定使用监视器锁去做,这样 调度 同步都是内核来完成的,应用态的语言包括我们应用程序员都是没有改造权限的,golang 做的一个事情就是把原本让内核干的事情,都夺回来来让应用层自己来干,因为操作系统其实 提供像信号这种中断机制,就类似 CPU 提供给操作系统的时钟中断一样,也就是 go 调度器 干了原本归内核的事情… 干了之后 我们就可以自己优化策略,或者用其它的抽象 例如管道 来替代锁这种抽象
夜读这一期是讲了调度模型,但是关于 runtime 的具体还是没有涉及到,比如上面几个问题似乎在这个视频当中找不到答案。
#18 runtime 当然 还有 垃圾回收器… 忘记讲了 垃圾回收也是 runtime 一部分
“runtime 运行在 OS 线程上,比如 pthread 。参见 https://github.com/golang/gofrontend/blob/master/libgo/runtime/go-libmain.c#L208
go 进程应该是 goroutine 吧。这个真不好回答。有很多个,但至少有一个 runtime.main,至于最多有多少个,如果 golang 实现者考虑过这个问题的话,会有一个可以设置的参数的,可以用环境变量 GOMAXPROCS 限制 OS 线程数。”
--------------------------
请问上述内容,是不是可以认为你觉得是一个 OS 线程就有一个 runtime 呢?
是的,关于 runtime 包含的东西很多,比如你说的 channel 、调度模型、垃圾回收等,但是我还是觉得 Runtime 最开始的几个内容还是不太清晰,如 runtime 究竟运行在哪里呢?如果按照 的说法,是不是在 GMP 模型中 runtime 运行在每个 M 系统线程上?每个 M 都承载着一个 runtime 进行并发调度?还是说 runtime 只是运行在一个初始的系统线程 M0 上,然后再进行指挥调度所有的线程、协程等;
而且关于 Runtime 的创建及运行过程,Google 上面几乎也搜索不到,都是在将 GMP 的调度模型等等的内容,并没有涉及到最开始的部分内容。如果你觉得可以了解到的一些博客,请推荐一下啊,谢谢大佬了。
#21 这个你要了解 runtime 每个细节的话,只能去看源代码了, 我只是提供一些大致上的思路,因为很多东西道理是一样的,例如垃圾回收 肯定要暂停 goroutine 维持 变量之间的引用关系的一致性,不然你一边在修改变量引用关系 又进行垃圾回收 是不现实的…
runtime.collector()
老哥头像能发一张吗。。
没看过源码,但是看过一个垃圾回收的教程,我觉得应该会有类似这样的实现(上面还没打完就发出去了也不能删除)
movl $4096, %rdi
call runtime.alloc # 调用 runtime 中的垃圾回收函数
movl $1, -4($rax)
movl $2, -8($ax)
…
这不知道要咋发啊。
var a []int // 申请一个动态数组
a = append(a, 22222222)
a = append(a, 33333333)
类似这样的 golang 代码会转成上面你的汇编,另外汇编是可以调用 c 函数的,只要按照 c 调用约定就可以了。runtime.alloc 无非就是调用 c 的 malloc 函数申请一个 4096 字节的空间,然后返回指针地址给 %rax(调用约定)。
-----go tool compile -S main.go 虽然没看太懂,但是大概是这样
0x0046 00070 (main.go:5) CALL runtime.growslice(SB) #调用 runtime 函数得到一段堆内存空间
0x004b 00075 (main.go:5) MOVQ 40(SP), AX
0x0050 00080 (main.go:5) MOVQ 48(SP), CX
0x0055 00085 (main.go:5) MOVQ 56(SP), DX
0x005a 00090 (main.go:5) MOVQ $22222222, (AX) # 根据指针偏移写入到堆内存
0x0061 00097 (main.go:6) LEAQ 2(CX), BX
0x0065 00101 (main.go:6) CMPQ DX, BX
0x0068 00104 (main.go:6) JCS 125
0x006a 00106 (main.go:6) MOVQ $33333333, 8(AX)(CX*8) # 根据指针偏移写入到堆内存
有原链接吗。。
#27 然后调度代码的话 每一个 M 都是有自己的 线程栈幁的,调度的时候 有调度队列,不同的 M 可以去其它 M 的队列里面去偷 goroutine 执行,这可以理解吗?因为暂停 goroutine 的话 只要保存 goroutine 的协程栈幁就好了…
#21 你如果要完全理解 goroutine 代码 调度器代码 谁控制了线程( CPU )控制权的话,最好去读 X86 汇编跟抢占式调度原理,因为无论协程 线程都是抽象的概念,只要能保存其上下文 例如线程只要操作系统保存 RSP RBP IP 寄存器 CPU 就能在线程之间 不断来回切换,另外操作系统在进程之间切换 可能还要修改 CR3 寄存器 因为每个进程的内存地址空间不一样,但是对于协程来讲,我不清楚协程内部是如何维护栈幁的,但是原理肯定是相同的,无非是保存当前运行状态的寄存器跟各种上下文的东西 以便切换回来的时候 使用…
感谢
#29 这个要看是用 引用计数 还是 可达性分析,如果是引用计数的话 那就是跟 python 一样,每个对象有一个引用计数器,当它变成 0 的时候 这样就能回收,如果是可达性分析,那就要判断当前程序运行时这个状态哪些对象是不能被回收的,例如我 A->B->C 三个方法调用 C 方法在执行磁盘 IO,此时 A 跟 B 的临时变量引用或者间接引用的堆上面的内存空间 你是没法标记回收的,如果标记回收了 C 方法从内核返回回来 A 跟 B 发现自己的临时变量或者对象都不见了…那就出问题了,还有一些全局变量也是不能被回收的,因为全局变量的生命周期可能跨越整个程序的运行周期。
老哥建个微信群呗,拉点 v 友跟你学习学习
#38 我有吹水群的 你发下 id 我拉你进去…
对于静态语言而言 runtime 其实就是一个帮助函数库( runtime.go )。
golang 用户代码编译成汇编,然后链接这个帮助函数库(runtime.go)。 最后得到一整个完整的二进制可执行文件。
所以 runtime 和你自己写的 golang 代码并没啥太大的区别。用户代码调用 runtime 中的帮助函数,函数执行完了返回。 用户代码调用 import 中的函数,函数执行完了就返回了。都一样的呗。
我要进群,qq 吗?
看到了深不可测的技术 ( ps:我要进群)
#42 没有什么深不可测的… 你如果去读一下 linux 0.12 的代码 基本上都懂了… 微信群…
不是。
runtime 其实是 runtime dependence 运行时依赖。
你写了一个空的 main.main 函数,编译器套件隐式链接的那一部分就是 runtime,恰好这一部分也是 runtime 包。
在函数层面上,runtime 包里面的函数都是 runtime ;在操作系统层面上,你看到的 go 程序执行的线程都是 runtime 包里面的函数通过系统调用创建的。
你写的代码只是被 runtime.mian 调用而已,当你编译链接出一个 golang 编写的程序,runtime 包被隐式的链接进去,不需要你在代码里 import,因为所以的其他代码都依赖 runtime 包。
我说不好回答时,既要考虑你想要问什么,又要考虑我知道什么,还要考虑篇幅等。程序没有进内存前只是函数,你问“运行”当然是进内存后了,这些 runtime 函数都被 OS 线程调用执行,所以我回到 runtime 跑在 OS 线程上。但是有可能,你是想问 runtime 是在哪里调用的。不是你的函数调用 runtime,而是 runtime 调用你的函数,先执行 runtime,runtime 系统调用创建 OS 线程后,再调用 main.main 中你的函数。
gccgo 的 runtime 另当别论。
有啥优雅,又不能创建线程,死坑货 go 。我都明知道这行代码是阻塞的,它就是不让我创建线程跑,在协程跑,浪费时间。
这面试官以前用 Java 的?…
明明主楼问的 runtime,歪到并发模型还能歪这么多楼。这里就是 i 知乎吗,i 了 i 了。
wx: MTMxNzM2ODAwMTg=
谢谢大佬的回答~
我觉得你回答的很符合问题了,谢谢大佬。
是的,认识到了,理解来说就是一个库。
#45 为什么要创建线程,而且使用读写磁盘文件这种阻塞式 IO 的话,goroutine 封装了 syscall,会自动分离 M 跟 G,自动创建线程去应付这种阻塞式 IO,不比你手动去创建线程强 100 倍?
#47
i 就 i 了吧,每个人对 runtime 这个概念理解完全不一样,但是我们一般网上交流 runtime 无非是讨论 程序语言编译后的二进制文件在运行时提供的方方面面的功能,例如 程序的栈幁 调度模式(抢占 /非抢占) 调度单位 调度策略 内存回收策略 变量的生命周期 语言元信息的反射功能, 这都算作语言的 runtime,我不知道你所说的 runtime 是什么 runtime
#47 可能你有一个更好的对 runtime 的解释,愿听其详吧…
#45 另外后面的版本,即使你写了个死循环,现在 go 调度器也可以基于信号机制来实现抢占调度了
老哥,wx: MTMxNzM2ODAwMTg=
我知道有 go 是封装了所有的 IO,但是你调用一些本地函数,如第三方 so 库之类的,它们内部的读写文件和网络请求还是阻塞的。它是遇到了阻塞之后,会切换任务到其他线程中,如果所有的线程都阻塞才会启动新线程。这部分的逻辑是有性能消耗的,我明明知道一个操作是阻塞的,我手动创建线程去执行,就避免其他的运行任务队列的线程阻塞
好的,感谢
你可以了解下
我想加一下微信群 wx: R29waGVyLTE5OTU=
关于Golang(Go语言)Runtime的几个问题,以下是我的专业解答:
-
Golang Runtime的核心功能:
- Goroutine管理:包括创建、调度和终止Goroutine。
- 内存管理:自动进行内存分配和垃圾回收,提供内存统计功能。
- 系统信息获取:如获取CPU数量、Go版本等。
-
如何避免Golang程序运行时出现内存泄漏:
- 合理使用内存,避免过度分配。
- 利用Golang的垃圾回收机制自动管理内存。
- 定期检查内存使用情况,使用
runtime.MemStats
等工具进行内存统计和分析。
-
处理数组越界的方法:
- 在访问数组时,确保索引在数组范围内。
- 在代码中添加范围检查,避免越界访问。
-
如何保证Golang程序的并发安全:
- 使用互斥锁(Mutex)保护共享资源,避免竞态条件。
- 利用Channels进行Goroutine间的通信和数据同步,确保数据一致性和安全性。
综上所述,了解和善用Golang Runtime的功能和特性,对于编写高效、稳定的Go程序至关重要。希望以上解答能对你有所帮助。如有其他问题,请随时提问。