go协程、线程的本质,如何协调运作
协程与线程
线程在创建、切换、销毁时候,需要消耗CPU的资源。
协程就是将一段程序的运行状态打包, 可以在线程之间调度。减少CPU在操作线程的消耗
协程、线程、进程 这块网上非常多文章讲了,就不多叙述了。
归纳下:
进程用分配内存空间
线程用来分配CPU时间
协程用来精细利用线程
协程的本质是一段包含了运行状态的程序 后面介绍后,会对这个概念更好理解
协程的本质
上面讲了 ,协程的本质就是 一段程序的运行状态的打包:
func Do() {
for i := 1; i <= 1000; i++ {
fmt.Println(i)
time.Sleep(time.Second)
}
}
func main() {
go Do()
select {}
}
例如上面这段代码,开了一个协程,然后一直循环打印。
假设程序都还有很多其他的协程也在工作,发现这个协程工作太久了,系统会进行切换别的协程,现在这个协程会放入协程队列中。
问题:要做到这点,协程需要怎么保存这个执行状态?
- 需要一个函数的调用栈,记录执行了那些函数,(例子中只有一个,正常情况下会是很多函数相互调用) 函数执行完后,还需要回到上层函数,所以要保存函数栈信息。
- 需要记录当前执行到了 那行代码,不能把多执行,也不能少执行那句代码,不然程序会不可控。
- 需要一个空间,存储整个协程的数据,例如变量的值等。
协程的底层定义
在runtime的runtim2.go中
type g struct {
// 只留了少量几个,里面有非常多的字段。
stack stack // 调用栈
m *m // 协程关联了一个m (GMP)
sched gobuf // 协程的现场
goid uint64 // 协程的编号
atomicstatus atomic.Uint32 // 协程的状态
}
type gobuf struct {
sp uintptr // 当前调用的函数
pc uintptr // 执行语句的指针
g guintptr
ctxt unsafe.Pointer
ret uintptr
lr uintptr
bp uintptr // for framepointer-enabled architectures
}
// 栈的定义
type stack struct {
lo uintptr // 低地址
hi uintptr // 高地址
}
整体下:
假如有这么一段代码:
func do3() {
fmt.Println("dododo")
}
func do2() {
do3()
}
func do1() {
do2()
}
func main() {
go do1()
time.Sleep(time.Hour)
}
在do2断点:
能看到下方的调用栈中,会自动插入一个 goexit 在栈头。
小结下,整体的结构如下:
总结:
runtime 中,协程的本质是一个g 结构体
stack:堆栈地址
gobuf:目前程序运行现场
atomicstatus: 协程状态
线程的底层 m
操作系统的线程是由操作系统管理,这里的m只是记录线程的信息。
截取部分代码:
type m struct {
g0 *g // goroutine with scheduling stack
id int64 // id号
morebuf gobuf // gobuf arg to morestack
curg *g // 当前运行的g
p puintptr // attached p for executing go code (nil if not executing go code)
mOS // 系统线程信息
}
go 是go程序启动创建的第一个协程,用来操控调度器的,第二个是主协程,可以看下 go启动那篇
小结:
runtime 中将操作系统线程抽象为 m结构体
g0:g0协程,操作调度器
curg:current g,目前线程运行的g
mOs:操作系统线程信息
如何工作
协程究竟是如何在 线程中工作的 ?
先讲总结,然后跟着总结往下看:
这是单个线程的循环,没有P的存在。
1. schedule() 是线程获取 协程的入口方法
线程通过执行 g0协程栈,获取 待执行的 协程
也就是意味着,每次线程执行 这个schedule
方法,就意味着会切换一个 协程。
这个结论很重要,后面 协程调度时候,会大量看到调用这个方法。
在runtime的 proc.go下面能看到这个方法,这里只留了两行代码,
只和目前逻辑相关的,这个方法后面还要多次读
func schedule() {
gp, inheritTime, tryWakeP := findRunnable() // blocks until work is available
execute(gp, inheritTime)
}
这里的gp就是 待执行的g
可以和上面的图对上,这里去 `Runnable` 找一个协程。然后,调用 `execute` 方法。
至于怎么去找的,知道GMP的肯定都知道,这个后面聊。
也只有部分代码,和这里业务相关的
func execute(gp *g, inheritTime bool) {
mp := getg().m //获取m,线程的抽象
mp.curg = gp // 还记得 m的定义 里面有个 当前的 g 在这里赋值了
gp.m = mp // g的定义也有个 m,这里也赋值了
gogo(&gp.sched)
}
到gogo
func gogo(buf *gobuf) // 只有定义,说明是汇编实现的,而且是平台相关的
// func gogo(buf *gobuf)
// 这里把 g的gobuf传过去了,gobuf 存着 sp 和 pc ,当前的执行函数,和执行语句
// 到这里就基本对应上了
// restore state from Gobuf; longjmp
TEXT runtime·gogo(SB), NOSPLIT, $0-8
MOVQ buf+0(FP), BX // gobuf
MOVQ gobuf_g(BX), DX
MOVQ 0(DX), CX // make sure g != nil
JMP gogo<>(SB)
// 插入了 goexit的栈针 然后开始运行业务
TEXT gogo<>(SB), NOSPLIT, $0
get_tls(CX)
MOVQ DX, g(CX)
MOVQ DX, R14 // set the g register
MOVQ gobuf_sp(BX), SP // restore SP 插入了 goexit的栈针
MOVQ gobuf_ret(BX), AX
MOVQ gobuf_ctxt(BX), DX
MOVQ gobuf_bp(BX), BP
MOVQ $0, gobuf_sp(BX) // clear to help garbage collector
MOVQ $0, gobuf_ret(BX)
MOVQ $0, gobuf_ctxt(BX)
MOVQ $0, gobuf_bp(BX)
MOVQ gobuf_pc(BX), BX
JMP BX
在运行业务之前 jmp bx
,都还在 g0的协程栈上。
目前,已经把开始执行,到执行都整理了一遍,但是,没有讲 goexit
插入 到底有什么作用?
经验丰富的伙伴大致能猜到, 当执行完了协程的任务后,需要回到
schedule
方法中, 线程重新去执行别的协程,这就是goexit
的作用
goexit
汇编实现
TEXT runtime·goexit(SB),NOSPLIT|TOPFRAME,$0-0
BYTE $0x90 // NOP
CALL runtime·goexit1(SB) // 去调用 goexit1 这个方法
// Finishes execution of the current goroutine.
func goexit1() {
mcall(goexit0) // 通过mcall 调用goexit0
}
// mcall switches from the g to the g0 stack and invokes fn(g),
// 切换到 g0 栈
func mcall(fn func(*g))
就是只,上面的都是在 业务协程中,运行的,到这里,开始使用 g0栈去运行,goexit0
// goexit continuation on g0.
func goexit0(gp *g) {
mp := getg().m
pp := mp.p.ptr()
casgstatus(gp, _Grunning, _Gdead)
gcController.addScannableStack(pp, -int64(gp.stack.hi-gp.stack.lo))
if isSystemGoroutine(gp, false) {
sched.ngsys.Add(-1)
}
gp.m = nil
locked := gp.lockedm != 0
gp.lockedm = 0
mp.lockedg = 0
gp.preemptStop = false
gp.paniconfault = false
gp._defer = nil // should be true already but just in case.
gp._panic = nil // non-nil for Goexit during panic. points at stack-allocated data.
gp.writebuf = nil
gp.waitreason = waitReasonZero
gp.param = nil
gp.labels = nil
gp.timer = nil
schedule()
}
// 对结束的g进行了一些置0的工作,然后调用了 schedule()
schedule()
意味着 为现在的线程,切换协程。
到此,和上面的图都对应上了。但是目前还是单线程,多线程时候,是如何工作了,下篇再聊。