Golang 调度器 GPM模型

Golang 调度器 GPM模型

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

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

在这里插入图片描述

上图为一个CPU通过调度器切换CPU时间轴的情景。如果未来满足宏观上每个进程/线程是一起执行的,则CPU必须切换,每个进程会被分配到一个时间片中。

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

在这里插入图片描述

对于Linux操作系统来言,CPU对进程和线程的态度是一样的,如图1.3所示,如果系统的CPU数量过少,而进程/线程数量比较庞大,则相互切换的频率也就会很高,其中中间的切换成本越来越大。这一部分的性能消耗实际上是没有做在对程序有用的计算算力上,所以尽管线程看起来很美好,但实际上多线程开发设计会变得更加复杂,开发者要考虑很多同步竞争的问题,如锁、资源竞争、同步冲突等。

2 协程来提高CPU利用率

那么如何才能提高CPU的利用率呢?多进程、多线程已经提高了系统的并发能力,但是在当今互联网高并发场景下,为每个任务都创建一个线程是不现实的,因为这样就会出现极大量的线程同时运行,不仅切换频率高,也会消耗大量的内存:进程虚拟内存会占用4GB(32位操作系统),而线程也要大约4MB。大量的进程或线程出现了以下两个新的问题。

  • (1)高内存占用。
  • (2)调度的高消耗CPU。

工程师发现其实可以把一个线程分为“内核态”和“用户态”两种形态的线程。所谓用户态线程就是把内核态的线程在用户态实现了一遍而已,目的是更轻量化(更少的内存占用、更少的隔离、更快的调度)和更高的可控性(可以自己控制调度器)。用户态中的所有东西内核态都看得见,只是对于内核而言用户态线程只是一堆内存数据而已。

一个用户态线程必须绑定一个内核态线程,但是CPU并不知道有用户态线程的存在,它只知道它运行的是一个内核态线程(Linux的PCB进程控制块),如下图所示:

在这里插入图片描述

如果将线程再进行细化,内核线程依然叫 线程(Thread) ,而用户线程则叫 协程(Co-routine) 。操作系统层面的线程就是所谓的内核态线程,用户态线程则多种多样,只要能满足在同一个内核线程上执行多个任务,例如Co-routine、Go的Goroutine、C#的Task等。

既然一个协程可以绑定一个线程,那么能不能多个协程绑定一个或者多个线程呢?接下来有3种协程和线程的映射关系,它们分别是 N : 1 关系、 1 : 1 关系和 M : N 关系。

3 N比1关系

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

在这里插入图片描述

N:1关系面临的几个问题如下:

  • (1) 某个程序用不了硬件的多核加速能力。
  • (2) 某一个协程阻塞,会造成线程阻塞,本进程的其他协程都无法执行了,进而导致没有任何并发能力。

4 1比1关系

1个协程绑定1个线程,这种方式最容易实现。协程的调度都由CPU完成了,虽然不存在N:1的缺点,但是协程的创建、删除和切换的代价都由CPU完成,成本和代价略显昂贵。协程和线程的1:1关系如图所示。

在这里插入图片描述

5 M比N关系

M个协程绑定1个线程,是 N: 11 : 1 类型的结合,克服了以上两种模型的缺点,但实现起来最为复杂。同一个调度器上挂载M个协程,调度器下游则是多个CPU核心资源。协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程,所以针对 M : N 模型的中间层的调度器设计就变得尤为重要,提高线程和协程的绑定关系和执行效率也变为不同语言在设计调度器时的优先目标。

在这里插入图片描述

6 Go语言的协程goroutine

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

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

Goroutine的特点,占用内存更小(几KB)和调度更灵活(runtime调度)。

7 被废弃的goroutine调度器

Go语言目前使用的调度器是2012年重新设计的,因为之前的调度器性能存在问题,所以使用4年就被废弃了,那么先来分析一下被废弃的调度器是如何运作的。

通常用符号G表示Goroutine,用M表示线程。接下来有关调度器的内容均采用图1.8所示的符号来统一表达。

早期的调度器是基于M:N的基础上实现的,图1.9是一个概要图形,所有的协程,也就是G都会被放在一个全局的Go协程队列中,在全局队列的外面由于是多个M的共享资源,所以会加上一个用于同步及互斥作用的锁。

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

不难分析出来,老调度器有以下几个缺点:

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

在这里插入图片描述

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

面对之前调度器的问题,Go设计了新的调度器。在新调度器中,除了 M(线程)G(协程) ,又引进了 P(处理器)

处理器包含了运行Goroutine的资源,如果线程想运行Goroutine,必须先获取P,P中还包含了可运行的G队列。

在这里插入图片描述

9 GPM模型

在Go中,线程是运行Goroutine的实体,调度器的功能是把可运行的Goroutine分配到工作线程上。
在GPM模型中有以下几个重要的概念,如图1.12所示。

在这里插入图片描述

  • (1)全局队列(Global Queue): 存放等待运行的G。全局队列可能被任意的P去获取里面的G,所以全局队列相当于整个模型中的全局资源,那么自然对于队列的读写操作是要加入互斥动作的。
  • (2)P的本地队列: 同全局队列类似,存放的也是等待运行的G,但存放的数量有限,不超过256个。新建G′时,G′优先加入P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。
  • (3)P列表: 所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个。
  • (4)M: 线程想运行任务就得获取P,从P的本地队列获取G,当P队列为空时,M也会尝试从全局队列获得一批G放到P的本地队列,或从其他P的本地队列“偷”一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。

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

10 有关P和M个数的问题

  • (1) P的数量由启动时环境变量 $GOMAXPROCS 或者由 runtime 的方法 GOMAXPROCS( ) 决定。这意味着在程序执行的任意时刻都只有 $GOMAXPROCS 个 Goroutine在 同时运行。
  • (2)M的数量由Go语言本身的限制决定,Go程序启动时会设置M的最大数量,默认为10000个,但是内核很难支持这么多的线程数,所以这个限制可以忽略。runtime/deBug 中的 SetMaxThreads( ) 函数可设置M的最大数量,当一个M阻塞了时会创建新的M。

M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来。

11 有关P和M何时被创建

  • (1) P创建的时机在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。
  • (2) M创建的时机是在当没有足够的M来关联P并运行其中可运行的G的时候。例如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,如果此时没有空闲的M,就会去创建新的M。

12 调度器的设计策略

策略一:复用线程

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

1)偷取(Work Stealing)机制

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

这里需要注意的是,偷取的动作一定是由P发起的,而非M,因为P的数量是固定的,如果一个M得不到一个P,那么这个M是没有执行的本地队列的,更谈不上向其他的P队列偷取了。

2)移交(Hand Off)机制

当本线程因为G进行系统调用阻塞时,线程会释放绑定的P,把P转移给其他空闲的线程执行,如图1.14所示,此时若在M1的GPM组合中,G1正在被调度,并且已经发生了阻塞,则这个时候就会触发移交的设计机制。GPM模型为了更大程度地利用M和P的性能,不会让一个P永远被一个阻塞的G1耽误之后的工作,所以遇见这种情况的时候,移交机制的设计理念是应该立刻将此时的P释放出来

如图1.15所示,为了释放P,所以将P和M1、G1分离,M1由于正在执行当前的G1,全部的程序栈空间均在M1中保存,所以M1此时应该与G1一同进入阻塞的状态,但是已经被释放的P需要跟另一个M进行绑定,所以就会选择一个M3(如果此时没有M3,则会创建一个新的或者唤醒一个正在睡眠的M)进行绑定,这样新的P就会继续工作,接收新的G或者从其他的队列中实施偷取机制。

策略二:利用并行

GOMAXPROCS 设置P的数量,最多有 GOMAXPROCS 个线程分布在多个CPU上同时运行。 GOMAXPROCS 也限制了并发的程度,例如 GOMAXPROCS=核数/2 ,表示最多利用一半的CPU核进行并行。

策略三:抢占

在Co-routine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个Goroutine最多占用CPU 10ms,防止其他Goroutine无资源可用,这就是Goroutine不同于Co-routine的一个地方。

Co-routine(C语言中的协程),用户态线程。
coroutine 是基于 ucontext 的一个 C 语言协程库实现

策略四:全局G队列

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

13 go func() 调度流程

如果执行一行代码 go func( ) ,则在GPM模型上的概念里会执行哪些操作。

(1)通过 go func( ) 创建一个Goroutine,

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

(3)G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,则会从全局队列进行获取,如果从全局队列获取不到,则会向其他的MP组合偷取一个可执行的G来执行,如图1.20所示。

(4)一个M调度G执行的过程是一个循环机制,如图1.21所示。

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

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

14 调度器的生命周期

在Go语言调度器的GPM模型中还有两个比较特殊的角色,它们分别是M0和G0。

M0

(1)启动程序后的编号为0的主线程。

(2)在全局命令runtime.m0中,不需要在heap堆上分配。

(3)负责执行初始化操作和启动第1个G。

(4)启动第1个G后,M0就和其他的M一样了。

G0

(1)每次启动一个M,创建的第1个Goroutine就是G0。

(2)G0仅用于负责调度G。

(3)G0不指向任何可执行的函数。

(4)每个M都会有一个自己的G0。

(5)在调度或系统调度时,会使用M切换到G0,再通过G0调度。

(6)M0的G0会放在全局空间。

一个Goroutine的创建周期如果加上M0和G0的角色,则整体的流程如图1.24所示。

下面跟踪一段代码,对调度器里面的结构做一个分析,代码如下:

package mainimport "fmt"func main() {fmt.Println("Hello world")
}

整体的分析过程如下:

(1)runtime创建最初的线程 M0Goroutine G0 ,并把二者关联。

(2)调度器初始化:初始化M0、栈、垃圾回收,以及创建和初始化由 GOMAXPROCSP 构成的 P列表 ,如图1.25所示。

(3)示例代码中的 main( ) 函数是 main.main ,runtime中也有1个main()函数 runtime.main ,代码经过编译后, runtime.main 会调用 main.main ,程序启动时会为 runtime.main 创建Goroutine,称为 Main Goroutine ,然后把 Main Goroutine 加入P的本地队列。

(4)启动M0,M0已经绑定了P,会从P的本地队列获取G,并获取 Main Goroutine

(5)G拥有栈,M根据G中的栈信息和调度信息设置运行环境。

(6)M运行G。

(7)G退出,再次回到M获取可运行的G,这样重复下去,直到 main.main 退出, runtime.main 执行Defer和Panic处理,或调用 runtime.exit 退出程序。

调度器的生命周期几乎占满了一个Go程序的一生, runtime.main 的Goroutine执行之前都是为调度器做准备工作, runtime.main 的Goroutine运行才是调度器的真正开始,直到 runtime.main 结束而结束。

参考

  • https://blog.csdn.net/flynetcn/article/details/126628952
  • https://blog.csdn.net/weixin_43495948/article/details/129415438
  • 《深入理解Go语言》刘丹冰

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/715477.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

chatgpt:还有哪些人工智能和科技值得关注?

今天,很多人的目光都被ChatGPT吸引,其实,人工智能的范围很大,远不止ChatGPT或者其他自然语言的处理工具。所以说不管ChatGPT的结果如何,人工智能依然是未来。 那么在ChatGPT之外,还有没有什么值得关注的人…

在网页上踢球:打造我自己的python(Django)足球网站

足球不仅仅是球场上的90分钟。它是一个不断发展的故事,一个全球球迷社群的粘合剂,一个数据和热情交织的世界。作为一名开发者和球迷,我决定将这两大爱好结合起来,用 Django 打造一个足球网站,让球迷们能够追踪他们最爱…

Unity AI生成全景图制作天空盒

现在的AI很强大。 其中,有这样一个网站,通过输入提示词,选择某种风格就可以为你生成360全景图。 网页链接 一、生成全景图 打开网页后,如图: 勾选,点击CONFIRM。 点击GET STARTED,进入主页。…

机器人定位——里程计Odom

根据两个车轮的轮速去估计当前的车的定位 我将提供一个更详细完整的模型来描述两轮差速机器人的里程计数。 我们假设机器人的两个轮子的半径分别为r1和r2,两个轮子的转速分别为ω1和ω2。机器人的轮距为L,指的是两个轮子中心之间的距离。 首先&#x…

Git LFS配置

当你需要克隆一个包含通过 Git Large File Storage (LFS) 管理的大文件的仓库时,确保 Git LFS 已经在你的系统上安装并正确配置是很重要的。这样,当你执行 git clone 命令时,Git LFS 跟踪的文件也会被正确地下载。以下是在 macOS 上进行配置和…

Stable Cascade-ComfyUI中文生图、图生图、多图融合基础工作流分享

最近 ComfyUI对于Stable Cascade的支持越来越好了一些,官方也放出来一些工作流供参考。 这里简单分享几个比较常用的基础工作流。 (如果还没有下载模型,可以先阅读上一篇Stable Cascade升级,现在只需要两个模型) &a…

python数据分析numpy基础之argmax求数组最大值索引

1 python数据分析numpy基础之argmax求数组最大值索引 python的numpy库的argmax()函数&#xff0c;用于获取沿指定轴的最大值的索引。 用法 numpy.argmax(a, axisNone, outNone, *, keepdims<no value>)描述 argmax()返回沿指定轴的最大值的索引。 入参axis表示指定轴…

Docker技术概论(5):Docker网络

Docker技术概论&#xff08;5&#xff09; Docker网络 - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog…

基于QT和Visa的安捷伦(keysight)34970A温度采集

在以前的文章中&#xff0c;描述了如何在labview开发读取34970A仪器采集的温度。 也描述了如何安装keysight IO Libraries Suits. 那么本文更进一步&#xff0c;描述QT平台c语言开发软件&#xff0c;读取34970A仪器采集的温度。 以下是c代码&#xff0c;因为采集耗费时间长&…

C++虚函数调用规则

C虚函数调用规则 基类、派生类结构&#xff1a; class Foo { public:virtual void print() {cout << "Foo" << endl;} }; class Bar : public Foo { public:virtual void print() {cout << "Bar" << endl;} };1.通过对象直接调用…

AcWing 895. 最长上升子序列(线性dp)

问题描述 给定一个长度为N NN的数列&#xff0c;求数值严格单调递增的子序列的长度最长是多少。 输入格式&#xff1a; 第一行包含整数N NN。 第二行包含N NN个整数&#xff0c;表示完整序列。 输出格式&#xff1a; 输出一个整数&#xff0c;表示最大长度。 数据范围 1 ≤…

【C++提高编程】

C提高编程 C提高编程1 模板1.1 模板的概念1.2 函数模板1.2.1 函数模板语法1.2.2 函数模板注意事项1.2.3 函数模板案例1.2.4 普通函数与函数模板的区别1.2.5 普通函数与函数模板的调用规则1.2.6 模板的局限性 1.3 类模板1.3.1 类模板语法1.3.2 类模板与函数模板区别1.3.3 类模板…

备战蓝桥杯---动态规划的一些思想1

话不多说&#xff0c;直接看题&#xff1a; 目录 1.双线程DP 2.正难则反多组DP 3.换个方向思考&#xff1a; 1.双线程DP 可能有人会说直接贪心&#xff1a;先选第1条的最优路径&#xff0c;再选第2条最优路径。 其实我们再选第1条时&#xff0c;我们怎么选会对第2条的路径…

FastJson中“$ref 循环引用检测”的问题

今天在测试时&#xff0c;错误停留在了以下的代码行 Object object new ObjectMapper().readValue(JSON.toJSONString(procInst.getForm()), Object.class); 报错信息&#xff1a;com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field &quo…

linux命令行与shell脚本大全——学习笔记(1-4章)

第一章、第二章 查看运行层级 runlevel 目前有7个层级&#xff0c;3是有联网的多用户模式&#xff0c;5是配有GUI的多用户模式&#xff0c;等等 第三章 启动shell 查看/etc/passwd文件&#xff0c;可以看到每个用户的默认shell程序&#xff0c;如: christine:x:1001:1001:…

面条机水箱低液位提醒功能如何实现

光电液位传感器在面条机水箱低液位功能的实现中发挥着重要作用。该技术通过光学原理和分离式设计&#xff0c;实现了面条机水箱液位的精准检测和智能控制&#xff0c;为面条生产提供了稳定的保障。 采用分离式液位传感器&#xff0c;将菱镜部分设计直接置于面条机水箱上&#…

nvidia a100-pcie-40gb环境安装

1.conda create --name torch_li python3.8 2. conda install pytorch1.7.1 torchvision0.8.2 torchaudio0.7.2 cudatoolkit11.0 -c pytorch 环境测试&#xff1a;torch.cuda.is_available() 3.conda remove -n torch_li --all 4.pip install opencv-python-headless 5.pip ins…

SOCKS55代理与Http代理有何区别?如何选择?

在使用IPFoxy全球代理时&#xff0c;选择 SOCKS55代理还是HTTP代理&#xff1f;IPFoxy代理可以SOCKS55、Http协议自主切换&#xff0c;但要怎么选择&#xff1f;为解决这个问题&#xff0c;得充分了解两种代理的工作原理和配置情况。 在这篇文章中&#xff0c;我们会简要介绍 …

overleaf上传到arxiv 参考文献无法引用(?)

记一下overleaf上传到arxiv的bug 参考文献无法引用&#xff08;&#xff1f;&#xff09; 因为需要上传bbl文件而不是bib 用overleaf生成bbl 另外需要将bbl和txt的文件名设置成一样的

Linux笔记--解压缩

一、tar指令 Linux打包文件通常以.tar结尾&#xff0c;压缩文件以.gz(.bz2)结尾。通常压缩和打包是一起进行的&#xff0c;打包压缩后文件后缀名一般为.tar.gz。 z∶使用gzip进行解压缩 j:使用bzip2进行解压缩 c: create&#xff0c;创建文件 x : extract&#xff0c;解压 v:…