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,一经查实,立即删除!

相关文章

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

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

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

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

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

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

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

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

【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

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

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

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

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

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

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

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

RocketMQ消息积压如何处理

在高并发的场景下,由于消息产生速度超过消费速度,可能会导致消息积压的问题。本文将介绍 RocketMQ 消息积压的原因和如何处理积压问题。 什么是消息积压 消息积压是使用 MQ 消息队列系统中,最常见的一种性能问题。如下图所示,当生…

手动实现一个简单的 HTTP 请求

本文我们通过 Socket,写一个 HTTP 协议,直观的感受一下上篇文章中的请求和响应。 定义 socket server 通过上篇文章,我们知道 HTTP 协议底层是通过 Socket 实现的,所以我们先通过 socket 定义一个 server import socket#初始化 …

复试PAT乙级day34

1111~1115 1113 很难,看了题解 人类习惯用 10 进制,可能因为大多数人类有 10 根手指头,可以用于计数。这个世界上有一种叫“钱串子”(学名“蚰蜒”)的生物,有 30 只细长的手/脚,在它们的世界里…

算法题目跟连系列之“手把手刷链表”

第一道 题目:https://leetcode.cn/problems/partition-list/description/ 86 Partition List 这个题解决的时候,无非就是把链表中小于X的元素摘出来形成一个链表,同时也把大于等于X的元素摘出来形成另外一个链表。最后把这两个链表合并。这个…

卷积神经网络介绍

卷积神经网络(Convolutional Neural Networks,CNN) 网络的组件:卷积层,池化层,激活层和全连接层。 CNN主要由以下层构造而成: 卷积层:Convolutional layer(CONV)池化层&#xff1a…

docker报错 fatal error: runtim: out of memory

fatal error: runtim: out of memory 真无语了 系统内存也够用 原来是虚拟机的不够用了 (原本1g已经加到2g还是会报错) 直接3台虚拟机都加到4g

多线程(进阶四:线程安全的集合类)

目录 一、多线程环境使用ArrayList 二、多线程环境使用队列 三、多线程环境使用哈希表 1、HashMap 2、Hashtable 3、ConcurrentHashMap (1)缩小了锁的粒度 (2)充分使用了CAS原子操作,减少一些加锁 (3)针对扩容操作的一些优化(化整为零&#xff…

蓝桥杯Python B组练习——python复习2

蓝桥杯Python B组练习——python复习2 一、简介 复习python,参考书《Python编程从入门到实践》,[美]Eric Mathes著。前一部分见专栏——蓝桥杯Python B组练习 这一部分不全,不想写了 二、字典 1.一个简单的字典 来看一个游戏&#xff0…

模拟服务器响应的测试框架:moco

第1章:引言 大家好,我是小黑,在这篇博客中,咱们要聊聊Moco测试框架。这个框架,可不是一般的小伙伴,它在模拟服务器响应这块儿,可是有不少看家本领。 首先,Moco是啥呢?简…

stable diffusion webUI之赛博菩萨【秋葉】——工具包新手安裝与使用教程

stable diffusion webUI之赛博菩萨【秋葉】——工具包新手安裝与使用教程 AI浪潮袭来,还是学习学习为妙赛博菩萨【秋葉】简介——(葉ye,四声,同叶)A绘世启动器.exe(sd-webui-aki-v4.6.x)工具包安…

住房贷款利息退税笔记

应该缴税了才能退税,如果是学生,没有缴税应该是无法退税的。 产权证明 如果是商品房,没有取得房产证,那就是房屋预售合同 扣除年度 应选择上一年 扣除比例 没有结婚,选否 申报方式