goroutine

M:N模型

go创建M个线程,之后创建个N个goroutine会依附在M个线程上执行。

pstree 命令看进程下几个线程

同一时刻,一个线程只能跑一个goroutine,当goroutine发生阻塞(chan阻塞,mutex,syscall陷入内核)时,go的runtime会进行调度,让其他goroutine来继续执行,这样线程不会阻塞休眠。

GMP概念

G:goroutine。个数无限制

1
2
3
type g struct {
  
}

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。

内存分配原理
堆栈&逃逸分析
  1. goroutine自己的栈
  2. 全局堆空间用来动态分配

栈内存由编译器自动分配和释放,存储函数入参和局部变量,随函数创建而创建,随函数返回而销毁。

栈从高位地址向下生长。

堆是由编译器和工程师共同分配,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。