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 main
 
import "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语言》刘丹冰

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

腾讯云幻兽帕鲁服务器使用Linux和Windows操作系统,对用户的技术要求有何不同?

腾讯云幻兽帕鲁服务器使用Linux和Windows操作系统对用户的技术要求有何不同? 首先,从操作界面的角度来看,Windows操作系统相对简单易操作,适合那些偏好使用图形化界面操作的用户。而Linux操作系统则需要通过命令行完成&#xff0…

网络爬虫部分应掌握的重要知识点

目录 一、预备知识1、Web基本工作原理2、网络爬虫的Robots协议 二、爬取网页1、请求服务器并获取网页2、查看服务器端响应的状态码3、输出网页内容 三、使用BeautifulSoup定位网页元素1、首先需要导入BeautifulSoup库2、使用find/find_all函数查找所需的标签元素 四、获取元素的…

图论 - 二分图(染色法、匈牙利算法)

文章目录 前言Part 1:染色法判定二分图1.题目描述输入格式输出格式数据范围输入样例输出样例 2.算法 Part 2:匈牙利算法求二分图的最大匹配1.题目描述输入格式输出格式数据范围输入样例输出样例 2.算法 前言 本篇博客将介绍两种二分图有关的算法&#xf…

CSS【详解】居中对齐 (水平居中 vs 垂直居中)

水平居中 内部块级元素的宽度要小于容器(父元素) 方案一&#xff1a;文本居中对齐&#xff08;内联元素&#xff09; 限制条件&#xff1a;仅用于内联元素 display:inline 和 display: inline-block; 给容器添加样式 text-align:center<!DOCTYPE html> <html lang&q…

微软为金融界带来革命性突破——推出Microsoft 365中的下一代AI助手:Microsoft Copilot for Finance

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

阿里云搭建私有docker仓库(学习)

搭建私有云仓库 首先登录后直接在页面搜索栏中搜索“容器镜像服务” 进入后直接选择个人版&#xff08;可以免费使用&#xff09; 选择镜像仓库后创建一个镜像仓库 在创建仓库之前我们先创建一个命名空间 然后可以再创建我们的仓库&#xff0c;可以与我们的github账号进行关联…

云原生架构技术揭秘:DevOps 技术打破开发运维壁垒,实现持续交付的变革之道

DevOps 是一套将软件开发&#xff08;Development&#xff0c;Dev&#xff09;和系统运维&#xff08;Operations&#xff0c;Ops&#xff09;相结合的实践&#xff0c;旨在缩短应用系统开发生命周期&#xff0c;提供高质量的持续交付。 —— 维基百科 DevOps 0、讲在前面 生…

鸿蒙Harmony应用开发—ArkTS声明式开发(通用属性:显隐控制)

控制组件是否可见。 说明&#xff1a; 从API Version 7开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 visibility visibility(value: Visibility) 控制组件的显隐。 卡片能力&#xff1a; 从API version 9开始&#xff0c;该接口支持在…

BP 神经网络原理

BP (Back Propagation) 神经网络是1986年由 Rumelhart 和 McClelland 为首的科学家提出的概念&#xff0c;是一种按照误差逆向传播算法训练的多层前馈神经网络&#xff0c;是应用最广泛的神经网络。 1 BP 神经网络的结构和传播规则 BP神经网络由 输入层、隐含层&#xff08;也…

Revit-二开之立面视图创建FilledRegion-(3)

在上一篇博客中介绍了FilledRegion的创建方法,这种方法通常只在平面视图中适用,在三维视图中也是无法创建的(目前研究的是这样的,如果有其他方法,请赐教)。 本片文章介绍一个下在立面视图中创建FilledRegion的方法,主要操作是在立面视图中拾取一个点,然后以该点为原点,…

javaweb day9 day10

昨天序号标错了 vue的组件库Elent 快速入门 写法 常见组件 复制粘贴 打包部署

PYTHON 自动化办公:压缩图片(PIL)

1、介绍 在办公还是学习过程中&#xff0c;难免会遇到上传照片的问题。然而照片的大小限制一直都是个问题&#xff0c;例如照片限制在200Kb之内&#xff0c;虽然有很多图像压缩技术可以实现&#xff0c;但从图像处理的专业来说&#xff0c;可以利用代码实现 这里使用的库函数是…

【Redis知识点总结】(一)——各种数据结构及其应用场景

Redis知识点总结&#xff08;一&#xff09;——基础数据类型及其应用场景 基础数据类型基础数据类介绍底层数据结构SDS&#xff08;简单动态字符串&#xff09;list&#xff08;双向链表&#xff09;ziplist&#xff08;压缩列表&#xff09;quicklist&#xff08;快速表&…

Unity3D学习之Lua热更新解决方案(二)XLua

文章目录 1 XLua概述2 xLua导入和AB包相关准备3 C#调用Lua3.1 Lua解析器3.2 文件加载重定向3.3 Lua解析器管理器3.3.1 重定向AB包内的Lua3.3.2 获得_G大表 3.4 全局变量的获取3.5 全局函数的获取3.5.1 无参无返回3.5.2 有参有返回3.5.3 多返回值3.5.4 变长参数 3.6 List和Dicti…

策略模式 详解 设计模式

策略模式 策略模式是一种行为型设计模式&#xff0c;它定义了一系列算法&#xff0c;将每个算法封装到具有共同接口的独立类中&#xff0c;并且使它们可以相互替换。 策略模式可以让算法的变化独立于使用算法的客户端。 主要解决&#xff1a; 在有多种算法相似的情况下&#…

Linux系统管理:虚拟机 Kali Linux 安装

目录 一、理论 1.Kali Linux 二、实验 1.虚拟机Kali Linux安装准备阶段 2.安装Kali Linux 2. Kali Linux 更换国内源 3. Kali Linux 设置固定IP 4. Kali Linux 开启SSH远程连接 5. MobaXterm远程连接 Kali Linux 三、问题 1.apt 命令 取代哪些 apt-get命令 一、理论…

Linux文本处理三剑客:awk

在Linux操作系统中&#xff0c;grep、sed、awk被称为文本操作“三剑客”&#xff0c;上两期中&#xff0c;我们将详细介绍grep、sed的基本使用方法&#xff0c;希望能够帮助到有需要的朋友&#xff0c;现在&#xff0c;我们继续学习awk。 虽然awk是一个Linux中常见的命令&…

C 嵌入式系统设计模式 17:静态优先级模式

本书的原著为&#xff1a;《Design Patterns for Embedded Systems in C ——An Embedded Software Engineering Toolkit 》&#xff0c;讲解的是嵌入式系统设计模式&#xff0c;是一本不可多得的好书。 本系列描述我对书中内容的理解。本文章描述嵌入式并发和资源管理模式之三…

Slicer学习笔记(六十五) 3DSlicer的医学图像数据增强扩展模块

1. 医学图像数据增强扩展模块 基于3D Slicer5.1.0 编写了一个测试医学图像的数据增强测试扩展模块。 扩展模块名&#xff1a;DataAugementation 项目地址&#xff1a;DataAugmentation 下载该项目后&#xff0c;可以将该扩展模块添加到3D Slicer的扩展中。 关于如何给3DSlicer…

【STA】多场景时序检查学习记录

单周期路径 建立时间时序检查 在时钟的有效沿到达触发器之前&#xff0c;数据应在一定时间内保持稳定&#xff0c;这段时间即触发器的建立 时间。满足建立时间要求将确保数据可靠地被捕获到触发器中。 建立时间检查是从发起触发器中时钟的第一个有效沿到捕获触发器中时钟后面…