runtime
goroutine
M:N模型
go创建M个线程,之后创建个N个goroutine会依附在M个线程上执行。
pstree 命令看进程下几个线程
同一时刻,一个线程只能跑一个goroutine,当goroutine发生阻塞(chan阻塞,mutex,syscall陷入内核)时,go的runtime会进行调度,让其他goroutine来继续执行,这样线程不会阻塞休眠。
GMP概念
G:goroutine。个数无限制
|
|
M: machine 工作线程。最大数量是10000,每个线程有自己的线程栈 ,每个线程均有一个g0用于找可用goroutine,以及创建销毁和调度。
P: processor 可以理解为队列,代表M所需的上下文环境。
P中有local cache,每个P都有一个M与之对应。
runtime不需要做集中式的goroutine调度,每个M会在对应P的local cache找待执行goroutine,没有的话再去global queue 中找或者其他P中偷。M很忙的话,P会同M解绑。
若分布不均衡,会去偷-work stealing算法。
创建goroutine
P的初始化:创建CPU核数的P,存储在scheduel-d的空闲链表p-idle。
M的创建:go func会触发wakeup机制,新的goroutine会唤醒一个P,有空闲的P,而没有闲着(spinning)的M,需要去唤醒或者创建一个M。
程序启动后,主线程会痛M0绑定。这个M是P0的M创建的。
M绑定的P没有可执行的goroutine时,会去按照优先级抢占任务,runtime.schedule。
内存分配原理
堆栈&逃逸分析
- goroutine自己的栈
- 全局堆空间用来动态分配
栈内存由编译器自动分配和释放,存储函数入参和局部变量,随函数创建而创建,随函数返回而销毁。
栈从高位地址向下生长。
堆是由编译器和工程师共同分配,runtimeGC释放,垃圾回收器扫描堆空间寻找不再使用的对象。
栈分配廉价,堆分配昂贵。
nginx日志库,栈上申请内存直接写盘。
go没有明确区分堆和栈,而是交给编译器决定在哪里分配内存。
连续栈/分段栈
栈会根据需要增长和收缩,最大值为1GB。分段栈会频繁alloc和free,hot split问题
后来版本修改为连续栈。分配2倍内存再拷贝。指针指向旧栈的重新指向新栈,再回收旧栈。
栈区使用率不足1/4,垃圾回收时会进行栈缩容。
性能优化
小对象结构体合并
结构体内能不用指针就不用,减少gc
bytes.Buffer
内存数据刷到网络或者文件使用bytes.Buffer空间留足
slice/map预分配
防止长调用栈
减少defer使用
避免频繁创建临时对象
使用sync.Pool 或者使用局部变量复用
字符串拼接用strings.Builder
不必要的memory copy
Readv、Writev 非连续内存不用memory copy直接发到socket里面,原本需要拷到大buffer里面刷进去,使用这个不用了。类似于mmap。
- 原文作者:nepp
- 原文链接:https://nepp-an.github.io/post/runtime/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。