Go学习笔记

堆和栈:函数里面的临时变量一般放到栈上,全局变量或者new创建的变量会放到堆上。堆和栈相比,堆适合不可预知大小的内存分配,但是分配速度较慢,可能带来内存碎片。而栈的内存分配速度很快,并且会自动释放。 逃逸分析:当一个对象的指针被多个方法或线程引用时,则称这个指针发生了逃逸。逃逸分析决定了这个变量是放到堆上还是栈上。 编译器会根据变量是否被外部引用来决定是否逃逸:如果变量在函数外部没有引用,则优先放到栈上; ,如果变量在函数外部存在引用,则必定放到堆上。查看变量是否发生逃逸:go build -gcflags '-m -l' main.go或者反编译go tool compile -S main.go

Go 的 GC 方式为追踪式:从根对象出发,根据对象之间的引用信息,一步步推进直到扫描完毕整个堆并确定需要保留的对象,从而回收所有可回收的对象。具体来说是无分代(对象没有代际之分)、不整理(回收过程中不对对象进行移动与整理)、并发(与用户代码并发执行)的三色标记清扫算法: 白色对象(可能死亡):未被回收器访问到的对象。在回收开始阶段,所有对象均为白色,当回收结束后,白色对象均不可达。 ,灰色对象(波面):已被回收器访问到的对象,但回收器需要对其中的一个或多个指针进行扫描,因为他们可能还指向白色对象。 ,黑色对象(确定存活):已被回收器访问到的对象,其中所有字段都已被扫描,黑色对象中任何一个指针都不可能直接指向白色对象。

  1. 内存消耗。一个goroutine的栈内存占用2KB,而一个线程则需要消耗1MB栈内存;
  2. 创建和销毁。线程是内核态的,创建和销毁对操作系统来说消耗比较大,而goroutine是用户态的,由go runtime管理,消耗非常小;
  3. 切换。线程切换需要保存很多寄存器状态,而goroutine切换只需要保存极少的寄存器,因此切换效率比线程高很多。
  1. G:goroutine,G 会在 M 上得到执行;
  2. M:machine 系统线程。G 需要调度到 M 上才能运行,M 是真正工作的人。M 保存了自身使用的栈信息、当前正在 M 上执行的 G 信息、与之绑定的 P 信息。当 M 没有工作可做的时候,在它休眠前,会“自旋”地来找工作:检查全局队列,查看 network poller,试图执行 gc 任务,或者从其他 P “偷”工作;
  3. P:processor 调度器,保存了本地可运行的 G 队列。一个 M 只有绑定 P 才能执行 goroutine,当 M 被阻塞时,整个 P 会被传递给其他 M 接管。

GPM 三足鼎力,共同成就 Go scheduler。G 需要在 M 上才能运行,M 依赖 P 提供的资源,P 则持有待运行的 G。你中有我,我中有你。

GMP 的核心思想是:

  1. 线程复用(work stealing, hand off);
  2. 限制同时运行(不包含阻塞)的线程数为 N,N 等于 CPU 的核心数目;
  3. 线程私有的 runqueues,并且可以从其他线程 stealing goroutine 来运行,线程阻塞后,可以将 runqueues 传递给其他线程。

Go 程序启动后,会给每个逻辑核心分配一个 P(Processor调度器);同时,会给每个 P 分配一个 M(Machine,表示内核线程),这些内核线程仍然由 OS scheduler 来调度。在初始化时,Go 程序会有一个 G(initial Goroutine),执行指令的单位。G 会在 M 上得到执行,内核线程是在 CPU 核心上调度,而 G 则是在 M 上进行调度。

为什么需要 P 这个组件,直接把 runqueues 放到 M 不行吗?

  1. P维护一个本地队列,避免全局队列带来的锁竞争;
  2. 当线程阻塞的时候,P可以把其他G分配给别的线程。

goroutine 调度的时机: 1.go 关键字创建;2.GC;3.系统调用;4.atomic,mutex,channel 等操作会使 goroutine 阻塞,因此会被调度走。