golang MPG模型

题外话之进程,线程和协程的联系和区别:

进程是获取资源的基本单位。

线程是调度的单位,线程出现的原因为:

  • 进程间的信息难以共享数据,父子进程并未共享内存,需要通过进程间通信(IPC),在进程间进行信息交换,性能开销较大。
  • 创建进程(一般是调用 fork 方法)的性能开销较大。

多线程比多进程之间更容易共享数据,在上下文切换中线程一般比进程更高效。

原因如下:

  • 线程之间能够非常方便、快速地共享数据。
    • 只需将数据复制到进程中的共享区域就可以了,但需要注意避免多个线程修改同一份内存。
  • 创建线程比创建进程要快 10 倍甚至更多。
    • 线程都是同一个进程下自家的孩子,像是内存页、页表等就不需要了。

协程是用户态的线程,从进程的堆中分配一段内存作为协程的栈。

线程的栈只有8MB,golang的协程的栈只有2-4kb。

协程的优势:

  • 节省 CPU:避免系统内核级的线程频繁切换,造成的 CPU 资源浪费。而协程是用户态的线程,用户可以自行控制协程的创建于销毁,极大程度避免了系统级线程上下文切换造成的资源浪费。
  • 节约内存:在 64 位的Linux中,一个线程需要分配 8MB 栈内存和 64MB 堆内存,系统内存的制约导致我们无法开启更多线程实现高并发。而在协程编程模式下,可以轻松有十几万协程,这是线程无法比拟的。
  • 稳定性:前面提到线程之间通过内存来共享数据,这也导致了一个问题,任何一个线程出错时,进程中的所有线程都会跟着一起崩溃。
  • 开发效率:使用协程在开发程序之中,可以很方便的将一些耗时的IO操作异步化,例如写文件、耗时 IO 请求等。

MPG模型

M:工作线程,P:逻辑处理器维护协程队列,G:协程

线程是运行 goroutine 的实体,调度器的功能是把可运行的 goroutine 分配到工作线程上。

全局队列(Global Queue):存放等待运行的 G。
P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去

Goroutine 调度器和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行。

MPG数量:

P的数量,由系统变量 GOMAXPROCS控制的,一般为cup 的核数。

M的数量,SetMaxThreads 参数设置上线为10000个,但是由于核数的限制,根本到不了这个限制。

生命周期:

P:在确定了最大量之后,运行时候会进行创建;

M:一个M关联一个P并运行其中维护的G,如果M都堵塞了,就会创建新的M来完成就绪的任务

G:在运行go func的时候生成新的G,完成之后结束。

调度器设计

设计的主要策略就是复用线程,避免繁琐的创建,销毁线程。

Work stealing 机制

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

Hand off 机制

本线程因为G进行系统调度而阻塞的时候,释放绑定的P,把P移交给其他空闲的M

利用并行

多个P分布在不同的CPU同时运行

抢占

G占用Cpu的时候只会占用10ms,然后让出cup,防止其他G饿死。

全局GQ

当P队列满时,新生成的G会在全局队列中存放,当M在其他P中偷取不到G时候会从GQ里获取。

一个典型的goroutine过程

  1. M中的G通过go func 创建一个G;
  2. G会优先存放到当前 P的队列中,如果P队列均已经满员,则这个G和P中一半的G都会存放到全局队列中;
  3. M在执行G时候阻塞,则调度器会把P与M取消关联,找到一个空闲的M(如果没有就创建一个来执行P中的任务)
  4. M调用结束的时候,会尝试获取一个空闲的P执行如果获取不到,则M休眠。
特殊的M0和G0

M0:是启动程序后的编号为 0 的主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,不需要在 heap 上分配,M0 负责执行初始化操作和启动第一个 G, 在之后 M0 就和其他的 M 一样了。

G0:是每次启动一个 M 都会第一个创建的 gourtine,G0 仅用于负责调度的 G,G0 不指向任何可执行的函数,每个 M 都会有一个自己的 G0。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0。

出处见:https://learnku.com/articles/41728

你可能感兴趣的:(golang MPG模型)