Golang 的 GMP:并发编程的艺术

2023-09-22 14:50:07

前言

在 Golang 的并发编程中,GMP 是一个重要的概念,它代表了 Goroutine、M(线程)和 P(调度器)。这个强大的三位一体的并发模型使得 Golang 在处理并发任务时非常高效和灵活。通过 GMP 的组合,Golang 实现了一种高效的并发模型。它充分利用了多核处理器的优势,并通过轻量级的 Goroutine 实现了高并发的编程模式。但是GPM到底是怎么工作的呢?今天这篇文章就为您解开GPM的神秘面纱。

调度器的由来

单进程系统

图片

早期的计算机都是单进程操作系统,各个进程之间都是顺序执行,也就是进程A执行完了才能执行进程B。

「对于cpu来说,进程和线程是一样的,这里我们就不讨论进程和线程的区别了」。

存在的问题
  • 单一执行流程,计算机只能一个任务一个任务的处理。
  • 如果进程A阻塞,会带来很多cpu浪费的时间。

多进程/线程操作系统

基于以上的问题,于是就出现了多进程/线程操作系统。

图片

  • 系统把cpu分成了一段一段的时间片(微妙级别)。
  • cpu在第一个时间片执行进程A,然后切换到进程B执行,再切换到进程C,一直这样轮询的执行。
  • 因为cpu被分成的时间片是微妙级别的,所以直观的感觉就是进程A,B,C是在同时执行的。
  • 多进程/线程操作系统的确解决了阻塞的问题,但是又出现了新的问题。
存在的问题

图片

  • 因为cpu需要不断地进程A,B,C之间切换,切换肯定避免不了各种复制,计算等消耗,所以在切换过程中浪费掉了很多时间成本,所以「进程/线程越多」,切换「成本就越大」,也就越「浪费」。
  • 在这种模式下运行CPU在切换动作上浪费的时间成本大概是40%,只有60%的时间是在执行程序。
  • 进程和线程对内存的占用是比较大的,在32位的操作系统中,进程占用的虚拟内存大概是4GB,现成占用内存大概是4M。

图片

协程的诞生

对于一个线程来说其实分为两部分,「用户空间」和「内核空间」。

图片

图片

  • 内核空间主要是指操作系统底层,包括进程开辟,分配物理内存资源,磁盘资源等。
  • 用户空间主要是编码业务逻辑部分。
  • 于是有人想到能不能把线程的内核空间和用户空间分开。并且让他们互相绑定在一起。
  • 对于cpu来说,只需要关注内核空间的线程就可以了。

当然如果只是这样把用户空间的协程和内核空间的线程一一绑定还是没有解决问题的,如果开启的比较多,那么对应的线程也会跟着一起增加,cpu频繁切换的问题还是没有解决,于是就引入了「调度器」的概念。

引入调度器来在各个协程之间切换,cpu只需要关注内核空间的线程即可,这样「解决了cpu在各个协程之间不断切换的问题」。

存在的问题

这样设计虽然解决了cpu频繁切换的问题,但是如果协程A发生了阻塞,肯定会导致协程B无法被执行。而且如果计算机是多核,那么是无法利用到多核的优势的。显然是不合理的。

对于多核的计算机,在内核空间可以开启多个线程(具体开启几个由计算内核决定,人为无法控制),所以问题的核心点就转移到了协程调度器上面,不管是什么语言,「协程调度器」做的越好,相对的「cpu利用率」也就越高。

图片

go对协程的处理

内存控制和灵活调度
  • 首先golang对协程改名为gorountine,并且把多余的空间都去掉,控制每个协程的内存在几KB大小,所以golang可以开启大量协程。
  • golang对协程的调度非常灵活,可以经常在各个协程之间切换。

图片

go对早期调度器的处理(GM模型)

golang在早起调度器处理是比较简单的,具体流程如下:

图片

  • 首先会有一个全局的go协程队列,并且加锁,防止资源竞争。
  • M获取锁之后会去尝试执行gorountine,执行完毕再把gorountine重新放回队列中。
GM模型存在以下问题
  • 创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。
  • M转移G会造成延迟和额外的系统负载。
  • 系统调用(cpu在M之间切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。
  • 比如我再一个G中又开辟了一个G1,那么G1和G当然在一个M上执行是比较合适的,因为存在一些共享内存,但是显然这种调度模式是无法做到的 基于以上问题,golang针对这块做了一些改进,也就是我们今天的主角,GMP模型。

GMP模型

GMP模型简介

GMP模型主要指的是G(gorountine协程),M(thread线程),P(processor处理器)之间的关系。

全局队列

存放等待运行的G。

P的本地队列
  • 存放等待运行的G。
  • P的本地队列存放的G是有数量限制的,一般是不超过256G。
  • 如果创建一个G,是会优先放在p的本地队列中,如果满了则会放到全局队列中去。
P列表
  • 在程序启动的过程时创建。
  • 最多有GOMAXPROCS个(可配置)。
  • 可以通过环境变量$GOMAXPROCS来设置P的个数,也可以在程序中通过runtime.GOMAXPROCS()来设置。
M列表
  • 当前操作系统分配到当前go程序的内核线程数。
  • go语言本身,限制M的最大数量是10000。
  • 可以通过runtime/debug包中的setMaxThreads来设置。
  • 如果有一个M阻塞,则会创建一个新的M。
  • 如果有M空闲,那么会回收或者睡眠。

调度器的设计策略

复用线程
work stealing机制

图片

  • M1对应的P上面G1正在执行,G2和G3处于等待中的状态。
  • M2对应的P处于空闲状态。

这种情况下M2对应的P会从M1对应的P的本地队列中把G3偷取过来执行,提高CPU的利用率,这种机制叫做「work stealing机制」。

hand off机制

图片

如果M1和M2都在正常执行,但是M1对应的G1发生了阻塞,那么势必会影响到G2的执行,那么GMP是如何解决的呢?

图片

  • golang会新创建一个M3,用来接管之前的P1剩下的G(G2)。
  • M1和G1进行绑定再继续执行,执行完毕之后把M1设置为睡眠状态等待下一次被利用,或者直接销毁。
并行利用

并行利用其实比较好理解,其实也就是开启了多少个P,P的个数是有GOMAXPROCS来决定的,一般都会设置为 「CPU核数/2」。

抢占策略

对于传统的co-routine来说,如果一个C和cpu进行了绑定,那么只有他主动释放,另外一个C才能和cpu进行绑定。但是在golang中,如果一个G和cpu进行了绑定,那么时间限制最多为10ms,另外一个G就可以直接和cpu绑定。

抢占策略。

全局队列

图片

  • 全局队列的本质是对work stealing的一种补充。
  • 如上图,M2对应的本地队列没有G,会优先从M1的本地队列中偷取。
  • 如果M1的本地队列中也没有G,那么就会从全局队列中去偷取G3。
  • 因为全局队列涉及到加锁和解锁,所以效率相对要低一些。

go的启动周期(M0和G0)

要想了解go的启动周期,首先得了解M0和G0的概念。

M0
  • 在一个进程中是唯一的。
  • 启动程序后编号为0的主线程。
  • 在全局变量runtime.m0中,不需要在heap上分配。
  • 负责初始化操作和启动第一个G。
  • 启动第一个G之后,M0就和其他的M一样了。
G0
  • 在一个线程中是唯一的。
  • 每次启动一个M,都会第一个创建的gorountine,就是G0。
  • G0仅仅用于负责调度其他G,G0不指向任何可执行的函数。
  • 每个M都会有一个自己的G0。
  • 在调度或者系统调用的时候,会使用M切换到G0来调度。
  • M0的G0会放在全局空间。

执行流程

package main
import "fmt"

func main() {
 fmt.Println("Hello World")
}

比如我们看上断代码的执行流程。

初始化操作

在执行到main函数之前,会有一些初始化的操作,比如创建M0,创建G0等等。

图片

执行具体函数

当执行main函数的时候,M0已经和其他的M是一样的了,main函数会进入M0对应的p的本地队列中,然后和M0绑定执行,如果执行超时(10ms),则会重新放到M0对应的本地队列中。一直到执行到exit或者panic为止。

图片

更多推荐

如何将办公文档导入到内容编辑区?

办公文档导入到内容编辑区功能让用户能够快速、轻松地将办公文档中的内容导入到内容编辑区中,以便进行进一步的编辑、排版和格式化。这个功能适用于多种场景,例如从Word文档、Excel表格或PowerPoint演示文稿中提取内容并将其导入到网页编辑器、博客平台或内容管理系统等。通过使用这个功能,用户可以省去手动复制和粘贴文本

通讯网关软件003——利用CommGate X2Mbt实现Modbus TCP访问OPC Server

本文介绍利用CommGateX2Mbt实现Modbus访问OPCServer。CommGateX2MBT是宁波科安网信开发的网关软件,软件可以登录到网信智汇(wangxinzhihui.com)下载。【案例】如下图所示,SCADA系统配置OPCServer,现在上位机需要通过Modbus主站软件来获SCADA的数据。【

Python的简单使用与应用

在当今互联网时代,网络爬虫成为了获取数据的重要工具之一。而使用代理IP进行爬虫操作,则是提高爬虫效率、绕过访问限制的利器。本文将向大家介绍Python代理IP爬虫的简单使用,帮助大家了解代理IP的原理、获取代理IP的方法,并探索其在实际应用中的无限可能。一、代理IP的原理和作用代理IP,顾名思义,即为代替本机IP进行网

计算机竞赛 深度学习 opencv python 公式识别(图像识别 机器视觉)

文章目录0前言1课题说明2效果展示3具体实现4关键代码实现5算法综合效果6最后0前言🔥优质竞赛项目系列,今天要分享的是🚩基于深度学习的数学公式识别算法实现该项目较为新颖,适合作为竞赛课题方向,学长非常推荐!🥇学长这里给一个题目综合评分(每项满分5分)难度系数:3分工作量:4分创新点:4分🧿更多资料,项目分享:h

【计算机网络】Tcp详解

文章目录前言Tcp协议段格式TCP的可靠性面向字节流应答机制超时重传流量控制滑动窗口(重要)拥塞控制延迟应答捎带应答标志位具体标志位三次握手四次挥手粘包问题TCP异常情况listen的第二个参数前言前面我们学习了传输层协议Udp,今天我们一起学习Tcp,Tcp比Udp复杂,但可靠,非常多的场景需要这种可靠。Tcp协议段

循环神经网络——上篇【深度学习】【PyTorch】【d2l】

文章目录6、循环神经网络6.1、序列模型6.1.1、序列模型6.1.2、条件概率建模6.1.2、代码实现6.2、文本预处理6.2.1、理论部分6.2.2、代码实现6.3、语言模型和数据集6、循环神经网络6.1、序列模型6.1.1、序列模型序列模型主要用于处理具有时序结构的数据,**时序数据是连续的,**随着时间的推移,

2023/9/17总结

VuedefineOptions为什么要使用defineOptions在有<scriptsetup>之前如果需要定义propsemit可以很容易的添加一个与setup平级的属性但是用了<scriptsetup>后就不能这样做了setup属性也就没有了,就不能添加与其平级的属性为了解决这个问题引入了defineProps

文件、预处理、位运算

10.2数据文件概述10.2.1ASCII文件与二进制文件ASCII文件就是“将需要保存到文件的信息使用ASCII字符表示,然后按照顺序将每个字符的ASCII码存储到文件中”。ASCII文件的优点是编码方式公开,可以被其它的文本编辑器打开;其缺点是效率比较低,信息冗余度高。二进制文件将数据在内存中的二进制形式原样存储到

十大排序算法及Java中的排序算法

文章目录一、简介二、时间复杂度三、非线性时间比较类排序冒泡排序(BubbleSort)排序过程代码实现步骤拆解演示复杂度选择排序(SelectionSort)排序过程代码实现步骤拆解演示复杂度插入排序(InsertionSort)排序过程代码实现步骤拆解演示复杂度二分插入排序(BinaryInsertionSort)代

数据可视化 -- ECharts 入门

文章目录引言1.ECharts的基本使用1.1ECharts的快速上手1.2相关配置讲解2.ECharts常用图表2.1图表1柱状图2.1.1柱状图的实现步骤2.1.2柱状图的常见效果2.1.3柱状图特点2.1.4通用配置2.2图表2折线图2.2.1折线图的实现步骤2.2.2折线图的常见效果2.2.3折线图的特点2.3

【iOS】单例模式

文章目录前言一、单例模式简介二、单例模式优缺点优点缺点三、模式介绍1.懒汉模式2.饿汉模式总结前言在最初进行OC的学习时笔者了解过单例模式的基本使用,现撰写博客加深对单例模式的理解一、单例模式简介单例模式是一种常见的设计模式,其主要目的是确保一个类只有一个实例,并提供全局访问点。这样就大大节省了我们的内存,防止一个实例

热文推荐