Go并发GMP调度模型

如何知道一个对象是分配在栈上还是堆上?

Go和C++不同,Go的逃逸分析是在编译器完成的;go局部变量会进行逃逸分析 。如果变量离开 作用域 后没有被引用 ,则优先分配到栈上,否则分配到堆上。那么如何判断是否发生了逃逸呢?

go build -gcflags '-m -m -l' xxx.go.

关于逃逸的可能情况:变量大小不确定,变量类型不确定,变量分配的内存超过用户栈最大值,暴露给了外部指针。如果变量内存占用较大时,优先放在堆上;如果函数外部没有引用,优先放在栈中;如果变量在函数外部存在引用;必定在堆中。

为什么有协程泄露(Goroutine Leak)?

协程泄漏是指协程创建之后没有得到释放。主要原因有:

  1. 缺少接收器,导致发送阻塞
  2. 缺少发送器,导致接收阻塞
  3. 死锁。多个协程由于竞争资源导致死锁。
  4. 创建协程的没有回收。

go内存管理机制。

golang内存管理基本是参考tcmalloc来进行的。go内存管理本质上是一个内存池,只不过内部做了很多优化:自动伸缩内存池大小,合理的切割内存块。

一些基本概念:

页Page:一块8K大小的内存空间。Go向操作系统申请和释放内存都是以页为单位的。

span : 内存块,一个或多个连续的 page 组成一个 span 。如果把 page 比喻成工人, span 可看成是小队,工人被分成若干个队伍,不同的队伍干不同的活。

sizeclass : 空间规格,每个 span 都带有一个 sizeclass ,标记着该 span 中的 page 应该如何使用。使用上面的比喻,就是 sizeclass 标志着 span 是一个什么样的队伍。

object : 对象,用来存储一个变量数据内存空间,一个 span 在初始化时,会被切割成一堆等大的 object 。假设 object 的大小是 16B , span 大小是 8K ,那么就会把 span 中的 page 就会被初始化 8K / 16B = 512 个 object 。所谓内存分配,就是分配一个 object 出去。

mheap

一开始go从操作系统索取一大块内存作为内存池,并放在一个叫mheap的内存池进行管理,mheap将一整块内存切割为不同的区域,并将一部分内存切割为合适的大小。

编辑切换为

mheap.spans :用来存储 page 和 span 信息,比如一个 span 的起始地址是多少,有几个 page,已使用了多大等等。

mheap.bitmap 存储着各个 span 中对象的标记信息,比如对象是否可回收等等。

mheap.arena_start : 将要分配给应用程序使用的空间。

mcentral

用途相同的span会以链表的形式组织在一起存放在mcentral中。这里用途用sizeclass来表示,就是该span存储哪种大小的对象。

找到合适的 span 后,会从中取一个 object 返回给上层使用。

mcache

为了提高内存并发申请效率,加入缓存层mcache。每一个mcache和处理器P对应。Go申请内存首先从P的mcache中分配,如果没有可用的span再从mcentral中获取。

参考资料:Go 语言内存管理(二):Go 内存管理

go如何进行调度的?GMP中状态流转。

Go里面GMP分别代表:G:goroutine,M:线程(真正在 CPU 上跑的),P:调度器Processor

调度器是M和G之间桥梁。

go进行调度过程:

  • 某个线程尝试创建一个新的G,那么这个G就会被安排到这个线程的G本地队列LRQ中,如果LRQ满了,就会分配到全局队列GRQ中;
  • 尝试获取当前线程的M,如果无法获取,就会从空闲的M列表中找一个,如果空闲列表也没有,那么就创建一个M,然后绑定G与P运行。
  • 进入调度循环。
  • 找到一个合适的G。
  • 执行G,完成以后退出。
  • work stealing 机制

当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。

  • hand off 机制

当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。

Go什么时候发生阻塞?阻塞时,调度器会怎么做。

  • 用于原子、 互斥 量或通道 操作导致goroutine阻塞,调度器将把当前阻塞的goroutine从本地运行队列LRQ换出,并重新调度其它goroutine;
  • 由于网络请求IO导致的阻塞,Go提供了网络轮询器(Netpoller)来处理,后台用epoll等技术实现IO多路复用。

其它回答:

  • channel 阻塞:当goroutine读写channel发生阻塞时,会调用gopark函数,该G脱离当前的M和P,调度器将新的G放入当前M。
  • 系统调用:当某个G由于系统调用陷入内核态,该P就会脱离当前M,此时P会更新自己的状态为Psyscall,M与G相互绑定,进行系统调用。结束以后,若该P状态还是Psyscall,则直接关联该M和G,否则使用闲置的处理器处理该G。
  • 系统监控:当某个G在P上运行的时间超过10ms时候,或者P处于Psyscall状态过长等情况就会调用retake函数,触发新的调度。
  • 主动让出:由于是协作式调度,该G会主动让出当前的P(通过GoSched),更新状态为Grunnable,该P会调度队列中的G运行。

Go中GMP有哪些状态?

G的状态:

_Gidle:刚刚被分配并且还没有被初始化,值为0,为创建goroutine后的默认值

_Grunnable: 没有执行代码,没有栈的所有权,存储在运行队列中,可能在某个P的本地队列或全局队列中(如上图)。

_Grunning: 正在执行代码的goroutine,拥有栈的所有权(如上图)。

_Gsyscall:正在执行系统调用,拥有栈的所有权,与P脱离,但是与某个M绑定,会在调用结束后被分配到运行队列(如上图)。

_Gwaiting:被阻塞的goroutine,阻塞在某个channel的发送或者接收队列(如上图)。

_Gdead: 当前goroutine未被使用,没有执行代码,可能有分配的栈,分布在空闲列表gFree,可能是一个刚刚初始化的goroutine,也可能是执行了goexit退出的goroutine(如上图)。

_Gcopystac:栈正在被拷贝,没有执行代码,不在运行队列上,执行权在系统线程上

_Gscan : GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在。

P的状态:

_Pidle :处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空

_Prunning :被线程 M 持有,并且正在执行用户代码或者调度器(如上图)

_Psyscall:没有执行用户代码,当前线程陷入系统调用(如上图)

_Pgcstop :被线程 M 持有,当前处理器由于垃圾回收被停止

_Pdead :当前处理器已经不被使用

M的状态:

自旋 线程:处于运行状态但是没有可执行goroutine的线程,数量最多为GOMAXPROC,若是数量大于GOMAXPROC就会进入休眠。

自旋 线程:处于运行状态有可执行goroutine的线程。

Pool

  • 应用场景(GC,堆,性能优化,常见)
    • Go是一个门有垃圾回收的语言,但是如果想要开发高性能的应用程序,就需要考虑GC带来的性能的影响,Go的GC是有一个STW时间的,而且大量创建在堆上的对象也会影响GC的时间,所以一般做性能优化的时候会考虑采用对象池的方式,比较常见的有数据库连接池,tcp长连接、动态buffer池等待。
  • 基本概念(由于需求,保存,线安,不可copy)
    • 正是基于以上的需求,Go提供了Pool数据类型。Pool用来保存一组可独立访问的临时对象。Pool本身是线程安全的,可以并发的存取对象。Pool一旦使用则不可再复制。
  • 基本用法(三方法NGP,New字段,Get、Put)
    • Pool结构体提供了三个方法,分别是New、Get、Put。Pool结构体有一个字段叫New,它的类型是一个无参数返回值为interface的函数,当Get方法获取不到对象时,会调用New方法创建一个新的对象,如果没有设置这个参数,那么Get方法在获取不到对象的时候会返回nil。Get方法用于在Pool中获取一个对象,Put方法用于存储一个对象到pool池当中。
  • 实现原理(https://zhuanlan.zhihu.com/p/616436531
  • 易错场景(元素很大、占内存大、不可回收、解决;内存浪费、解决)
    • 关于Pool的易错场景主要有两个,一个是内存泄漏,另一个是内存浪费。
      • 内存泄露是说由于疏忽或者错误导致未使用的内存没有被释放。比如在在动态buffer池当中的元素的底层数组很大,而且因为pool回收的机制,这些大的buffer可能不被回收,这就导致了内存泄漏问题。解决方案是在put时先判断内存大小,再决定是否要放回池子。
      • 第二个是内存浪费,这种情况是buffer很大,而实际需要的buffer通常很小,这是一种内存浪费现象,解决方案是将buffer池分成几个不同大小的池子,比如小于512byte的buffer占一个池子,小于1k的buffer占一个池子等等,这样就可以根据需要选择不同大小的buffer了。

个人博客网站:pengfei -- zpf的个人网站

相关推荐
Winston Wood3 分钟前
Java线程池详解
java·线程池·多线程·性能
Alive~o.04 分钟前
Go语言进阶&依赖管理
开发语言·后端·golang
手握风云-7 分钟前
数据结构(Java版)第二期:包装类和泛型
java·开发语言·数据结构
喵叔哟27 分钟前
重构代码中引入外部方法和引入本地扩展的区别
java·开发语言·重构
尘浮生33 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
不是二师兄的八戒1 小时前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
爱编程的小生1 小时前
Easyexcel(2-文件读取)
java·excel
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
计算机毕设指导62 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
Gu Gu Study2 小时前
枚举与lambda表达式,枚举实现单例模式为什么是安全的,lambda表达式与函数式接口的小九九~
java·开发语言