GMP 模型总结

简介: GMP 模型总结

GMP 原理与调度-地鼠文档 (topgoer.cn)Go面试必问——GMP调度模型详解_golang面试 gmp-CSDN博客

起源

早期的单进程操作系统,面临 2 个问题:

  1. 单一的执行流程,计算机只能一个任务一个任务处理。
  2. 进程阻塞所带来的 CPU 时间浪费。

多进程 / 线程时代有了调度器需求

在多进程 / 多线程的操作系统中,因为一个进程阻塞 cpu 可以立刻切换到其他进程中去执行,而且调度 cpu 的算法可以保证在运行的进程都可以被分配到 cpu 的运行时间片。这样从宏观来看,似乎多个进程是在同时被运行。

但新的问题就又出现了,进程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间,CPU 虽然利用起来了,但如果进程过多,CPU 有很大的一部分都被用来进行进程调度了。

协程来提高 CPU 利用率

多进程、多线程已经提高了系统的并发能力,但是在当今互联网高并发场景下,为每个任务都创建一个线程是不现实的,因为会消耗大量的内存 (进程虚拟内存会占用 4GB [32 位操作系统], 而线程也要大约 4MB)。

大量的进程 / 线程出现了新的问题

  • 高内存占用
  • 调度的高消耗 CPU

其实一个线程分为 “内核态 “线程和” 用户态 “线程。一个 “用户态线程” 必须要绑定一个 “内核态线程”,但是 CPU 并不知道有 “用户态线程” 的存在,它只知道它运行的是一个 “内核态线程”(Linux 的 PCB 进程控制块)。再去细化去分类一下,内核线程依然叫 “线程 (thread)”,用户线程叫 “协程 (co-routine)”. 既然一个协程 (co-routine) 可以绑定一个线程 (thread),那么能不能多个协程 (co-routine) 绑定一个或者多个线程 (thread) 上呢。

N 个协程绑定 1 个线程

N 个协程绑定 1 个线程,优点就是协程在用户态线程即完成切换,不会陷入到内核态,这种切换非常的轻量快速。但也有很大的缺点,1 个进程的所有协程都绑定在 1 个线程上

缺点:

  • 某个程序用不了硬件的多核加速能力
  • 一旦某协程阻塞,造成线程阻塞,本进程的其他协程都无法执行了,根本就没有并发的能力了。

1 个协程绑定 1 个线程

协程的调度都由 CPU 完成了,不存在 N:1 缺点

缺点:

  • 协程的创建、删除和切换的代价都由 CPU 完成,有点略显昂贵了。

M 个协程绑定 N 个线程

克服了以上 2 种模型的缺点,但实现起来最为复杂。

Go 语言的协程 goroutine

协程跟线程是有区别的,线程由 CPU 调度是抢占式的,协程由用户态调度是协作式的,一个协程让出 CPU 后,才执行下一个协程。

Go 为了提供更容易使用的并发方法,使用了 goroutine 和 channel。goroutine 来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被 runtime 调度,转移到其他可运行的线程上。最关键的是,程序员看不到这些底层的细节,这就降低了编程的难度,提供了更容易的并发。

Go 中,协程被称为 goroutine,它非常轻量,一个 goroutine 只占几 KB,并且这几 KB 就足够 goroutine 运行完,这就能在有限的内存空间内支持大量 goroutine,支持了更多的并发。虽然一个 goroutine 的栈只占几 KB,但实际是可伸缩的,如果需要更多内容,runtime 会自动为 goroutine 分配。

Goroutine 特点:

  • 占用内存更小(几 kb)
  • 调度更灵活 (runtime 调度)

原生的用户态线程,即协程,coroutinego里面的协程是goroutine

Goroutine 调度器的 GMP 模型

被废弃的 goroutine 调度器

仅有GM,用 G 来表示 Goroutine,用 M 来表示线程M 想要执行、放回 G 都必须访问全局 G 队列,并且 M 有多个,即多线程访问同一资源需要加锁进行保证互斥 / 同步,所以全局 G 队列是有互斥锁进行保护的。

老调度器有几个缺点:

  • 创建、销毁、调度 G 都需要每个 M 获取锁,这就形成了激烈的锁竞争
  • M 转移 G 会造成延迟和额外的系统负载。 比如当 G 中包含创建新协程的时候,M 创建了 G’,为了继续执行 G,需要把 G’交给 M’执行,也造成了很差的局部性,因为 G’和 G 是相关的,最好放在 M 上执行,而不是其他 M’。
  • 系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作,增加了系统开销。

Goroutine 调度器的 GMP 模型的设计思想

引进了P (Processor),它包含了运行 goroutine 的资源,如果线程想运行 goroutine,必须先获取 P,P 中还包含了可运行的 G 队列。

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

调度器的设计策略

复用线程:避免频繁的创建、销毁线程,而是对线程的复用。

  • work stealing 机制:当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。
  • hand off 机制:当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。

利用并行:GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。

抢占

  • coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程
  • 在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这就是 goroutine 不同于 coroutine 的一个地方。

全局 G 队列:在新的调度器中依然有全局 G 队列,但功能已经被弱化了,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。

go func () 调度流程

从上图我们可以分析出几个结论:

1、我们通过 go func () 来创建一个 goroutine

2、有两个存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列。新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中;

3、G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系。M 会从 P 的本地队列弹出一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会想其他的 MP 组合偷取一个可执行的 G 来执行;

4、一个 M 调度 G 执行的过程是一个循环机制

5、当 M 执行某一个 G 时候如果发生了 syscall 或其余阻塞操作,M 会阻塞,如果当前有一些 G 在执行,runtime 会把这个线程 M 从 P 中摘除 (detach),然后再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个 P;

6、当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列。如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。

总结(高度概括)

早期的单进程操作系统,面临 2 个问题:

  1. 单一的执行流程,计算机只能一个任务一个任务处理。
  2. 进程阻塞所带来的 CPU 时间浪费。

多进程 / 多线程的操作系统中,通过并发技术和 CPU 进程调度技术提高了运行的速度多,但是在当今互联网高并发场景下,大量的进程 / 线程出现了

  • 高内存占用
  • 调度的高消耗 CPU

对于高效的并发编程,需要一个质量非常高的调度器


再去细化去分类一下,发现内核线程依然叫 “线程 (thread)”,用户线程叫 “协程 (co-routine)”.一个线程分为 “内核态 “线程和” 用户态 “线程。一个 “用户态线程” 必须要绑定一个 “内核态线程”

Go 中,协程被称为 goroutine,它非常轻量,对应着用户线程 “协程 (co-routine)”的作用。


Goroutine 调度器的 GMP 模型,用 G 来表示 Goroutine,用 M 来表示线程,用 P 来表示调度器。

线程 M 是运行 goroutine 的实体,调度器的功能是把可运行的 goroutine 分配到工作线程上,G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系

GMP模型可以复用线程:避免频繁的创建、销毁线程,而是对线程的复用。

  • work stealing 机制:当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。
  • hand off 机制:当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。抢占:在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死。

被废弃的 goroutine 调度器,没有 P 的原因

  • 创建、销毁、调度 G 都需要每个 M 获取锁,这就形成了激烈的锁竞争
  • M 转移 G 会造成延迟和额外的系统负载。 比如当 G 中包含创建新协程的时候,M 创建了 G’,为了继续执行 G,需要把 G’交给 M’执行,也造成了很差的局部性,因为 G’和 G 是相关的,最好放在 M 上执行,而不是其他 M’。
  • 系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作,增加了系统开销。


相关文章
|
Linux C语言
LINUX error: Building GCC requires GMP 4.2+, MPFR 3.1.0+ and MPC 0.8.0+.
LINUX error: Building GCC requires GMP 4.2+, MPFR 3.1.0+ and MPC 0.8.0+.
721 0
LINUX error: Building GCC requires GMP 4.2+, MPFR 3.1.0+ and MPC 0.8.0+.
|
6天前
|
监控 Java Unix
深入理解GMP模型
深入理解GMP模型
67 0
|
5月前
|
Linux Go 调度
谈谈对 GMP 的简单认识
谈谈对 GMP 的简单认识
|
6月前
|
Unix 调度
Pthreads实验
Pthreads实验
26 0
|
9月前
|
并行计算 TensorFlow 算法框架/工具
Tensorflow 缺少 libcusolver.so.10 和 libcudnn.so.8 两个库的解决办法
Tensorflow 缺少 libcusolver.so.10 和 libcudnn.so.8 两个库的解决办法
153 0
|
10月前
|
Ubuntu 算法 Linux
移植Zlib,Libpng,FreeType详细步骤
移植Zlib,Libpng,FreeType详细步骤
314 0
|
机器学习/深度学习 Linux
Linux下安装gmp6.2.1的详细操作(深度学习)
Linux下安装gmp6.2.1的详细操作(深度学习)
364 0
Linux下安装gmp6.2.1的详细操作(深度学习)
|
运维 算法 大数据
异常检测:季节性ESD Python pip安装sesd库报错解决
百度、谷歌搜索了一圈,没有找到解决方案,自己根据经验终于搞定了。 大数据挑战赛用到了时间序列异常检测,发现了一个很好的方法:S-ESD, 季节性 esd 是一种在 twitter 上实现的异常检测算法:
180 0
异常检测:季节性ESD Python pip安装sesd库报错解决
|
并行计算 TensorFlow 算法框架/工具
编译安装汇总:nVidia驱动/CUDA/cuDNN/TensorRT/OpenCV/gstreamer/DeepStream/jpeglib等
编译安装汇总:nVidia驱动/CUDA/cuDNN/TensorRT/OpenCV/gstreamer/DeepStream/jpeglib等
150 0
|
存储 Java Unix
图像库libjpeg-turbo编译与实践
今天的主题就是libjpeg-turbo。
770 0
图像库libjpeg-turbo编译与实践