一、go的GC触发的机制:
1、在申请内存的时候,检查当前当前已分配的内存是否大于上次GC后的内存的2倍 2、监控线程发现上次GC的时间已经超过两分钟了 3、最后就是用户手动触发了,也就是调用 runtime.GC() 强制触发一次。
触发的时候,在主GC线程中就会走如下的GC流程: 1、stop the world,等待所有的M休眠;此时所有的业务逻辑代码都停止stw 2、标记:分配gc标记任务,唤醒 gcproc个 M(就是第一步休眠的那些),分别做这个,直到所有的M都做完,才结束;并且所有M再次进入休眠 3、清理:有一个单独的goroutine去清理已经标记的内存对象快 4、start the world,设置gcwaiting=0,唤醒所有的M(不会超过P个数) Go代码的自动内存管理,申请内存是在Go编译器的逃逸分析机制帮我们加上了,而 free 内存是由GC 机制来完成。
二、Go 构建了自己的 PMG 模型,
调度器是为了解决线程上下文切换的损耗: P:processor ,代表一个逻辑处理器(cpu),也就是执行代码的上下文环境。 M:machine ,代表一个内核线程,这个线程是操作系统来处理的,操作系统负责把它放置到一个 core 上去执行。 G:goroutine ,代表一个并发的代码片段。 简而言之,就是 P在M上执行G
三、Golang协程实现
g0是一个特殊的协程,用于执行调度逻辑,以及协程创建销毁等逻辑。调度逻辑都运行在g0栈;了理解协程栈,还需要简单了解下虚拟内存 调度器负责维护协程状态,获取一个可运行协程并执行; 有多种情况可能会触发协程调度:如读写管道阻塞了,如socket操作、系统调用、垃圾回收等 socket读写的处理,基于IO多路复用模型,比如epoll。Golang是这么做的,结构体pollServer封装了事件循环相关,Golang进程启动时,会创建pollServer,并启动事件循环;用操作系统的异步io模型,设置非阻塞,就是socket编程那几个函数; defer/panic/recover这几个关键字 A协程中触发panic,B协程中能否recover该panic呢? 发生panic后,只会遍历当前协程的defer链表,所以A协程中触发panic,B协程中肯定不能recover该panic。
内存管理: 一、操作系统内存管理:虚拟内存管理 CPU 在执行指令的时候,通过内存地址,将物理内存上的数据载入到寄存器,然后执行机器指令。 CPU访问数据的大致的顺序是 CPU –> L1 Cache –> L2 Cache –> L3 Cache –> 主存 –> 磁盘 主存与存储器之间以 page(通常是 4K) 为单位进行交换,cache 与 主存之间是以 cache line(通常 64 byte) 为单位交换的。可以通过数组遍历的方式来验证下缓存命中率的问题;局部性好的程序,可以提高缓存命中率,这对底层系统的内存管理是很友好的,可以提高程序的性能;
2、那最终编译出来的二进制文件,是如何被操作系统加载到内存中并执行的呢? 操作系统对内存的划分,每个区域用来做不同的事情,一个程序执行会分成代码段、全局变量段、堆、栈段的内存来放置程序执行的数据; https://www.jianshu.com/p/1ffde2de153f
四、Go 内存管理
Go 语言是利用操作系统的内存管理的这些特性来优化内存的,Go 的内存是自动管理的;Go的内存管理本质上就是一个内存池,只不过内部做了很多的优化。自动伸缩内存池大小,合理的切割内存块等。 mmap系统申请内存会进入内核态,调用开销大,可以在程序启动之初申请一块连续的内存,建立一个内存池 协程是直接从内存池里面申请内存的,在 mcentral 的前面又增加了一层 mcache,每一个 mcache 和每一个处理器(P) 是一一对应的; 其他优化: 有一些对象所需的内存大小是0,比如 [0]int, struct{},这种类型的数据根本就不需要内存,系统会直接返回一个统一的固定内存地址; Tiny对象:像 int32, byte, bool 以及小字符串等常用的微小对象,如果存储的对象小于 16B,这个空间会被暂时保存起来,下次分配时会复用这个空间; 大对象:最大的 sizeclass 最大只能存放 32K 的对象。如果一次性申请超过 32K 的内存,系统会直接绕过 mcache 和 mcentral,直接从 mheap 上获取;
go的内存管理这种设计之所以快,主要有以下4个优势和1个内存的浪费: 1、内存分配大多时候都是在用户态完成的,不需要频繁进入内核态。 2、每个 P 都有独立的 span cache,多个 CPU 不会并发读写同一块内存,进而减少 CPU L1 cache 的 cacheline 出现 dirty 情况,增大 cpu cache 命中率。 3、内存碎片的问题,Go 是自己在用户态管理的,在 OS 层面看是没有碎片的,使得操作系统层面对碎片的管理压力也会降低。 4、mcache 的存在使得内存分配不需要加锁。 5、当然这不是没有代价的,Go 需要预申请大块内存,这必然会出现一定的浪费,不过好在现在内存比较廉价,不用太在意。 这种设计比较通用,现在常见的 web 服务设计,为提升系统性能,一般都会设计成 客户端 cache -> 服务端 cache -> 服务端 db 这几层,也是金字塔结构。
逃逸分析:需要使用堆空间则逃逸,栈空间放函数内部变量、堆空间放程序变量 Go 编辑器会自动帮我们找出需要进行动态分配的变量,使用堆内存,编译器的这个分析过程就叫做逃逸分析。 熟悉堆栈概念可以让我们更容易看透 Go 程序的性能问题,并进行优化。 在一个函数中通过 dict := make(map[string]int) 创建一个 map 变量,其背后的数据是放在栈空间上还是堆空间上,是不一定的。这要看编译器分析的结果。 栈空间会随着一个函数的结束自动释放,堆空间需要 GC 模块不断的跟踪扫描回收 逃逸分析的缺陷:需要使用堆空间则逃逸,这没什么可争议的。但编译器有时会将不需要使用堆空间的变量,也逃逸掉。这里是容易出现性能问题的大坑。
五、系统调度
- 操作系统的线程调度,包括了多处理器和多核的存在; 如果你的程序是 CPU 密集的,那么上下文切换将会是性能的噩梦, IO 密集的工作,那么上下文切换会有一定的优势。一旦一个线程进入了阻塞态,另一个就绪态的线程就可以立马执行,不会让 CPU 闲着;
线程池是一个最好的解决方案。和线程的就绪状态配合; 需要在 CPU 核数和线程数量上寻找一个平衡,来使你的应用能够拥有最高的吞吐。当需要维持这个平衡时, 性能优化中很重要的一部分就是怎么才能让 CPU 更快的得到数据,来减少数据访问的延迟。写多线程应用时面对状态异变问题时,需要考虑 cache 系统的机制。 自己设计一个系统调度器:我们的目标是,如果有工作要做,就决不让 CPU 闲着
- goroutine 的调度器:Goroutine 不断的在 M 上做上下文切换 Go 调度器使你编写的 Go 程序并发性更好,性能更高。这主要是因为 Go 调度器很好的运用了系统调度器的机制原理。 你可以认为 Goroutine 是应用级别的线程,它在很多方面跟系统的线程是相似的。就像系统线程不断的在一个 core 上做上下文切换一样,
Go 调度器的上下文切换: Goroutine也拥有3 个高级状态。就像线程一样,一个 Goroutine有三种状态,阻塞态,就绪态,运行态 有 4 种事件会引起 Go 程序触发调度。这不意味着每次事件都会触发调度。Go 调度器会自己找合适的机会。 使用关键字 go 垃圾回收:因为GC操作是使用自己的一组 Goroutine 来执行,这些 Goroutine 需要一个M来运行。在GC过程中,暂停那些需要访问堆空间的Goroutine,运行那些不需要访问堆空间的。 系统调用
Go 是在 OS 层面上,将 IO/阻塞操作转换成了 CPU 操作。因为所有的上下文切换都发生在应用层,我们没有丢掉每次切换造成的 600 个指令损失; 在Go中,调度器试图用更少的线程,每个线程做更多的事情,帮助我们减少系统和硬件层的调度; Go 调度器把系统层面的 IO/阻塞 操作转换成了 CPU密集 操作来最大化每个 CPU 的能力; 你可以让每一个虚拟 Core 上都只跑一个线程来把所有事情做了,这是合理的。对于网络服务及其他不会阻塞系统线程的系统调用的服务来说,可以这样做。
系统调度器的机制和原理,对写一个多线程代码是重要的。了解Go语言调度器的机制,这对Go语言写出并发代码是重要的;
运行队列: 在 Go 调度器中有 2 个不同的执行队列:全局队列(Global Run Queue, 简称 GRQ)和本地队列(Local Run Queue,简称 LRQ)。 每一个 P 都会有一个 LRQ 来管理分配给 P 上的 Goroutine。这些 Goroutine 轮流被交付给 M 执行。 GRQ 是用来保存还没有被分配到 P 的 Goroutine,会有一个逻辑将 Goroutine 从 GRQ 上移动到 LRQ 上。
如何能知道,一个core上跑多少个 Goroutine 才能得到最大程度的吞吐量呢? CPU 密集型每一个core跑一个 Goroutine 会让速度最快,并发版本的add(数字相加的函数),add方法属于; 对于IO 密集型的工作,并行对提升性能没有多大的帮助(和并发提升差不多),CPU 密集型的正好相反。但是并不是所有 CPU 密集型的工作都适合并发的,比如冒泡排序 start和end CPU 密集型的工作,使用比机器核数数量更多的 Goroutine 会减慢工作,因为把 Goroutine 不断从系统线程上调来调去也是有成本。上下文切换会触发 Stop The World(简称STW)事件,因为在切换过程中你的工作不会被执行。
CPU多级缓存就看这6个关键词:CPU周期、内存、CPU寄存器、CPU的三级缓存、MESI协议、内存控制器 1、获取内存中的一条数据大概需要200多个CPU周期(CPU cycles),而CPU寄存器一般情况下1个CPU周期就够了 2、鉴于此,于是CPU设计者们就给CPU加上了缓存(CPU Cache)。如果需要对同一批数据操作很多次,那么把数据放至离CPU更近的缓存,会给程序带来很大的速度提升。 3、随着多核的发展,CPU Cache分成了三个级别:L1、 L2、L3。级别越小越接近CPU,所以速度也就更快,但同时也代表着容量越小。它们区别如下: L1是最接近CPU的,它容量最小、例如32K、速度最快,每个核上都有一个L1 Cache 就像数据库cache一样,获取数据时首先会在最快的cache中找数据,如果没有命中(Cache miss)则往下一级找,直到三层Cache都找不到,那只有向内存要数据了 4、MESI协议,现在主流的处理器都是用它来保证缓存的相干性和内存的相干性。M、E、S和I代表使用MESI协议时缓存行所处的四个状态; 5、如果c2需要读另外一个处理器c1的缓存行内容,c1需要把它缓存行的内容通过内存控制器(Memory Controller)发送给c2,c2接到后将相应的缓存行状态设为S。
linux的三种线程模型: 三种常用的多线程模型,包括主从模型、生产者消费者模型、高并发索引模型 资源、调度、性能层面,分析Linux线程与进程的差异