200. go GMP 模型相关知识博客

一些理解

内存使用:
虚拟内存是操作系统为每个进程提供的一种抽象,每个进程都有属于自己的、私有的、地址连续的虚拟内存,当然我们知道最终进程的数据及代码必然要放到物理内存上,那么必须有某种机制能记住虚拟地址空间中的某个数据被放到了哪个物理内存地址上,这就是所谓的地址空间映射,也就是虚拟内存地址与物理内存地址的映射关系,那么操作系统是如何记住这种映射关系的呢,答案就是页表,页表中记录的虚拟内存与物理内存的映射关系,有了页表就可以将虚拟地址转换为物理内存地址了,这种机制就是虚拟内存。

内存访问:
页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个cache就是TLB.
cpu 发出指令查某段内存的地址 mmu 通过页表查询出结果存放在 TLB 页表缓冲中.

进程切换开销:
    切换页表目录
    切换内核态堆栈
    切换硬件上下文
	刷新TLB
线程切换开销: 因为线程共享进程的资源所以只有下面两个
    切换内核态堆栈
    切换硬件上下文

对比: 发现主要少了, 而他们导致了什么?
	切换页表目录
	刷新TLB
切换页表目录导致, TLB失效,cpu访问内存时需要重新去内存读取页表数据, 而内存与cpu之间的数量级差了几百上千倍, 所以导致进程切换效率极差.

go 中的线程与 其他语言线程差别:

内核线程与用户线程其实是有关系的, 一个或者多个线程都关联一个或者多个内核线程, 其中内核线程是和cpu核心数量挂钩的, 基本是一对一关系, 每一个核心对应一个内核线程. 用户线程在用户空间中, 内核不关心你有多少个, 但是你在操作数据时必定会通过内核线程调用磁盘资源(比如读取文件), 用户线程没有权限操作系统资源. 同时根据cpu核心数问题, 如果每个内核线程之关联一个用户线程,呢么这就是真正的并发, 因为当存在多个用户线程对应同一内核线程时,  每个核心线程根据cpu的调度策略, 会在不同的用户线程之间切换(上下文消耗, 上下文表示该程序需要运行时的代码, 内存信息, 已经之前的状态还有寄存器中数据的切换保存), 不了解可以看下面两个博客.
在java等语言中, 如果启动成千上万的线程需要cpu不停地调度在不同的线程上以保证每个线程都能被处理到, 而这种切换带来的开销相对比较大的. 同时在现有的语言中每个请求占用一个线程, 系统的吞吐量取决于线程耗时. 其中JDBC操作尤其耗时, 因为在此过程中线程一直存于阻塞状态, 不同的线程等待时间不同, 造成资源利用不完全.
例子:
    最常见的例子就是JDBC(它是同步阻塞的),这也是为什么很多人都说数据库是瓶颈的原因。这里的耗时其实是让CPU一直在等待I/O返回,说白了线程根本没有利用CPU去做运算,而是处于空转状态。而另外过多的线程,也会带来更多的ContextSwitch开销。而协程的目的就是当出现长时间的I/O操作时,通过让出目前的协程调度,执行下一个任务的方式,来消除ContextSwitch上的开销。

根据别人的博客说明一个线程的大小应该在2M(栈空间+其他信息)左右. 

go 协程: 程序员根据需要手动通过代码在用户空间创建的轻量级线程, 这些线程被叫做协程, 他们由用户空间中的调度器去处理的, 当程序发生阻塞时调度器将当前协程挂起, 处理下一个协程, 由于协程的暂停完全由程序控制,发生在用户态上;而线程的阻塞状态是由操作系统内核来进行切换,发生在内核态上。(线程不能保证当前系统只有你运行的程序, 而协程可以保证我只处理你的程序, 由于一直处理同一程序空间数据, 所以协程共享用户空间的资源, 处理切换协程是寄存器上部分信息需要切换, 大部分信息不需要改变, 所以协程切换消耗非常小)

go goroutine GMP模型:
    G:表示goroutine,每个goroutine 都有自己的栈空间,定时器,初始化的栈空间在2k左右,空间会随着需求增长。# (协程, 记录任务信息, 等待M执行它, M是真正执行任务的线程)
    M:抽象化代表内核线程,记录内核线程栈信息,当goroutine 调度到线程时,使用该goroutine 自己的栈信息。# (相对于协程来说M表示内核线程[实际就是用户线程], 例如内核线程与用户线程关系, 同时M时真正执行任务的线程)
    P:代表调度器,负责调度goroutine,维护一个本地goroutine 队列,M 从P 上获得goroutine 并执行,同时还负责部分内存的管理。# (调取器,负责协程切换工作)

Goroutine位置:
    进程都有一个全局的G队列 # (全局G队列表示当前进程调度任务时会从其中选取goroutine去执行)
    每个P拥有自己的本地执行队列 #(调度器有自己的groutine队列, 当处理完成或者阻塞等需要切换操作, 会去全局G队列拉取任务到自己的队列, 并将当前G加入到阻塞队列中)
    有不在运行队列中的G
        处于channel阻塞态的G被放在sudog # ()
        执行该G的M会与P解绑,如系统调用 #(当M block on syscall时, p脱离与M绑定,寻找空闲状态的M,去继续执行剩余的G; 如果没有空闲状态的M, 就创建一个)
        为了复用,执行结束进入P的gFree列表中的G # (执行完成等待被调用, 类似于线程池)

Goroutine创建过程:
    获取或者创建新的Goroutine 结构体
        从处理器的gFree列表中查找空闲的Goroutine
        如果不存在空闲的Goroutine,会通过runtime.malg创建一个栈大小足够的新结构体
    将函数传入的参数移到Goroutine 的栈上
    更新Goroutine 调度相关的属性,更新状态为Grunnable
    返回的Goroutine 会存储到全局变量allgs中

将Goroutine 放到运行队列上:
    Goroutine 设置到处理器的runnext作为下一个处理器执行的任务
    当处理器的本地运行队列已经没有剩余空间时,就会把本地队列中的一部分Goroutine 和待加入的Goroutine 通过runtime.runqputslow添加到调度器持有的全局运行队列上

调度器行为:
    为了保证公平,当全局运行队列中有待执行的Goroutine 时,通过schedtick保证有一定几率会从全局的运行队列中查找对应的Goroutine
    从处理器本地的运行队列中查找待执行的Goroutine
    如果前两种方法都没有找到Goroutine,会通过runtime.findrunnable进行阻塞地查找Goroutine
        从本地运行队列、全局运行队列中查找
        从网络轮询器中查找是否有Goroutine 等待运行
        通过runtime.runqsteal尝试从其他随机的处理器中窃取待运行的Goroutine

在学习中看到比较好的博客

进程切换:
	2.https://blog.csdn.net/kevin_tech/article/details/104094028
进程线程协程精讲:
	3.https://www.cnblogs.com/Survivalist/p/11527949.html
GMP讲解:
    4.https://juejin.cn/post/6886321367604527112#heading-10   # 这个博客很好, 但是有些需要配合6中的阻塞部分细读
    5.https://www.cnblogs.com/codexiaoyi/p/14975236.html
golang 系统调用与阻塞处理:
    6.https://qiankunli.github.io/2020/11/21/goroutine_system_call.html
    1.https://www.cnblogs.com/zl1991/p/6543634.html  # syscall
原文地址:https://www.cnblogs.com/liuzhanghao/p/15375263.html