go如何启动一个goroutine
1 准备工作
1.1 创建一个main.go
package main import "fmt" type ss struct { a_b string b int64 } func x(c chan int) { b :{"", 4} if b.a_b != "" { return } c<-233 return } func main() { var c1 chan int c1 = make(chan int, 3) go x(c1) fmt.Printf("%d", <-c1) }
执行编译:go build -gcflags="all=-N -l" main.go
1.2 dlv执行调试
dlv exec main
2 goroutine的创建
2.1 单步调试go func()
将断点打到 go x(c1) 处
(dlv) bp Breakpoint runtime-fatal-throw (enabled) at 0x437a40,0x437940 for (multiple functions)() <multiple locations>:0 (0) Breakpoint unrecovered-panic (enabled) at 0x437da0 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1141 (0) print runtime.curg._panic.arg Breakpoint 23 (enabled) at 0x49e58f for main.main() ./main.go:18 (1) Breakpoint 24 (enabled) at 0x49e5ca for main.main() ./main.go:21 (1)
2.2 goroutine 的唤醒
单步调试到创建goroutine处
> main.main() ./main.go:21 (PC: 0x49e61a) main.go:21 0x49e609 eb0a jmp 0x49e615 main.go:21 0x49e60b 4889d7 mov rdi, rdx main.go:21 0x49e60e e82d82fcff call $runtime.gcWriteBarrierCX main.go:21 0x49e613 eb00 jmp 0x49e615 main.go:21 0x49e615 488b442448 mov rax, qword ptr [rsp+0x48] => main.go:21 0x49e61a e8413dfaff call $runtime.newproc main.go:22 0x49e61f 48c744242800000000 mov qword ptr [rsp+0x28], 0x0 main.go:22 0x49e628 488b442430 mov rax, qword ptr [rsp+0x30] main.go:22 0x49e62d 488d5c2428 lea rbx, ptr [rsp+0x28] main.go:22 0x49e632 e86979f6ff call $runtime.chanrecv1 main.go:22 0x49e637 440f117c2458 movups xmmword ptr [rsp+0x58], xmm15
将断点打到runtime.newproc 函数中的 if mainStarted 处,当继续单步时发现调用了wakep() 函数
(dlv) l 4246 Showing /usr/local/go/src/runtime/proc.go:4246 (PC: 0x0) 4241: func newproc(fn *funcval) { 4242: gp := getg() 4243: pc := getcallerpc() 4244: systemstack(func() { 4245: newg := newproc1(fn, gp, pc) // 创建 g 4246: 4247: pp := getg().m.p.ptr() 4248: runqput(pp, newg, true) // 将g放入运行队列 4249: 4250: if mainStarted {
=>4251: wakep() // 唤醒或者创建一个新的m和系统线程绑定p运行 g 4252: } 4253: }) 4254: } 4255: 4256: // Create a new g in state _Grunnable, starting at fn. callerpc is the (dlv) bp Breakpoint runtime-fatal-throw (enabled) at 0x437940,0x437a40 for (multiple functions)() <multiple locations>:0 (0) Breakpoint unrecovered-panic (enabled) at 0x437da0 for runtime.fatalpanic() /usr/local/go/src/runtime/panic.go:1141 (0) print runtime.curg._panic.arg Breakpoint 29 (enabled) at 0x49e58f for main.main() ./main.go:18 (1) Breakpoint 30 (enabled) at 0x49e5ca for main.main() ./main.go:21 (1) Breakpoint 31 (enabled) at 0x44242c for runtime.newproc.func1() /usr/local/go/src/runtime/proc.go:4250 (1)
可以发现当主线程也就是初始化main goroutine 运行main.main 的线程在将mainStarted设置为true后
// The main goroutine. func main() { mp := getg().m .... // Allow newproc to start new Ms. mainStarted = true // 将全局变量 mainStarted 设置true .... }
后续执行go func() 时执行 newproc 新建 goroutine 完成后,直接尝试wakep唤醒一个空闲的p 开启执行了
3、P的唤醒
3.1 获取空闲p
可以发现当主线程运行起来main goroutine 后,每当有新的goroutine创建都会尝试调用wakep 唤醒p
在wakep中会尝试去sched.pidle 全局调度管理变量sched 的空闲p链表获取空闲的p然后绑定m运行
// Tries to add one more P to execute G's. // Called when a G is made runnable (newproc, ready). // Must be called with a P. func wakep() { // Be conservative about spinning threads, only start one if none exist // already. if sched.nmspinning.Load() != 0 || !sched.nmspinning.CompareAndSwap(0, 1) { return } // Disable preemption until ownership of pp transfers to the next M in // startm. Otherwise preemption here would leave pp stuck waiting to // enter _Pgcstop. // // See preemption comment on acquirem in startm for more details. mp := acquirem() var pp *p lock(&sched.lock) pp, _ = pidlegetSpinning(0) if pp == nil { if sched.nmspinning.Add(-1) < 0 { throw("wakep: negative nmspinning") } unlock(&sched.lock) releasem(mp) return } // Since we always have a P, the race in the "No M is available" // comment in startm doesn't apply during the small window between the // unlock here and lock in startm. A checkdead in between will always // see at least one running M (ours). unlock(&sched.lock) startm(pp, true, false) // 将空闲p 传递过去运行 releasem(mp) }
3.2 开启一个m
startm 接收一个空闲p,获取一个空闲的m(没有就新建m 创建系统线程)绑定运行
func startm(pp *p, spinning, lockheld bool) { mp := acquirem() if !lockheld { lock(&sched.lock) } if pp == nil { if spinning { // TODO(prattmic): All remaining calls to this function // with _p_ == nil could be cleaned up to find a P // before calling startm. throw("startm: P required for spinning=true") } pp, _ = pidleget(0) if pp == nil { if !lockheld { unlock(&sched.lock) } releasem(mp) return } } nmp := mget() if nmp == nil { id := mReserveID() unlock(&sched.lock) var fn func() if spinning { // The caller incremented nmspinning, so set m.spinning in the new M. fn = mspinning } newm(fn, pp, id) // 新建m开启运行 if lockheld { lock(&sched.lock) } releasem(mp) return } if !lockheld { unlock(&sched.lock) } if nmp.spinning { throw("startm: m is spinning") } if nmp.nextp != 0 { throw("startm: m has p") } if spinning && !runqempty(pp) { throw("startm: p has runnable gs") } // The caller incremented nmspinning, so set m.spinning in the new M. nmp.spinning = spinning nmp.nextp.set(pp) // 将当前M要运行的p设定为传递过来的空闲p notewakeup(&nmp.park) // 唤醒通过futex 调用挂在 m.park.key 上的系统线程(唤醒M、唤醒睡眠的线程) releasem(mp) }
可以看见当存在空闲P时,就会尝试去获取(创建)一个M来运行P(g),
4、创建系统线程(LWP)
4.1 空闲P的调用栈
调用栈为 go func() -> newproc() -> wakep() -> startm() -> newm() -> newm1() -> newosproc()
> runtime.newosproc() /usr/local/go/src/runtime/os_linux.go:167 (PC: 0x433e3d) Warning: debugging optimized function 162: 163: // May run with m.p==nil, so write barriers are not allowed. 164: // 165: //go:nowritebarrier 166: func newosproc(mp *m) { => 167: stk := unsafe.Pointer(mp.g0.stack.hi) 168: /* 169: * note: strace gets confused if we use CLONE_PTRACE here. 170: */ 171: if false { 172: print("newosproc stk=", stk, " m=", mp, " g=", mp.g0, " clone=", abi.FuncPCABI0(clone), " id=", mp.id, " ostk=", &mp, "\n") (dlv) stack 0 0x0000000000433e3d in runtime.newosproc at /usr/local/go/src/runtime/os_linux.go:167 1 0x000000000043e150 in runtime.newm1 at /usr/local/go/src/runtime/proc.go:2253 2 0x000000000043e025 in runtime.newm at /usr/local/go/src/runtime/proc.go:2228 3 0x000000000043e539 in runtime.startm at /usr/local/go/src/runtime/proc.go:2411 4 0x000000000043e9d4 in runtime.wakep at /usr/local/go/src/runtime/proc.go:2541 5 0x000000000044243a in runtime.newproc.func1 at /usr/local/go/src/runtime/proc.go:4251 6 0x0000000000464689 in runtime.systemstack at /usr/local/go/src/runtime/asm_amd64.s:496 7 0x0000000000464620 in runtime.systemstack_switch at /usr/local/go/src/runtime/asm_amd64.s:463 8 0x00000000004423b1 in runtime.newproc at /usr/local/go/src/runtime/proc.go:4244 9 0x000000000043a2a5 in runtime.init.6 at /usr/local/go/src/runtime/proc.go:293 10 0x0000000000447852 in runtime.doInit at /usr/local/go/src/runtime/proc.go:6506 11 0x0000000000439ff3 in runtime.main at /usr/local/go/src/runtime/proc.go:199 12 0x0000000000466701 in runtime.goexit at /usr/local/go/src/runtime/asm_amd64.s:1598
关于新的M是如何创建绑定p与g 的开始运行可以查看此文:系统调用clone 创建新的系统线程 执行mstart 运行调度器
人生还有意义。那一定是还在找存在的理由
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?