Golang面试题四(GMP)

目录

[1.Goroutine 定义](#1.Goroutine 定义)

[2.GMP 指的是什么](#2.GMP 指的是什么)

3.GMP模型的简介

[全局队列(Global Queue)](#全局队列(Global Queue))

P的本地队列

P列表

M列表

4.有关P和M的个数问题

P的数量问题

M的数量问题

P和M何时会被创建

5.调度器P的设计策略

复⽤线程

[work stealing机制](#work stealing机制)

[hand off机制](#hand off机制)

利⽤并⾏

抢占

全局G队列

[6."go func()" 经历了什么过程](#6.“go func()” 经历了什么过程)

7.调度器的生命周期

M0

G0

8.Sysmon有什么作用

9.可视化GMP编程


1.Goroutine 定义

定义: Goroutine 是 Go 语言运行时系统(Runtime)管理的用户级线程(User-Level Thread),是一种可以在单个进程中并发执行的执行单元。每个 Goroutine 代表了一个独立的函数调用,可以在程序中并行地执行任务,而无需操作系统层面的线程(OS Thread)支持。

关键特性

  1. 轻量级:相较于操作系统原生线程,Goroutine 的创建、销毁和上下文切换(Context Switching)成本极低,使得开发者可以轻松地在程序中创建大量并发任务,实现高并发处理。

  2. 协程 :Goroutines 之间通过协作而非抢占式调度进行切换,这意味着一个 Goroutine 只有在主动放弃 CPU 时间片(如调用系统调用、阻塞在 I/O 操作或显式调用 sync 包中的同步原语)时,运行时才会调度其他等待的 Goroutine 执行。这种非抢占式的调度有助于减少线程上下文切换的开销。

  3. Go 语言原生支持 :在 Go 语言中,启动一个新的 Goroutine 非常简单,只需在函数调用前加上关键字 go,如 go someFunction()。编译器会负责将此函数封装为一个可以并发执行的 Goroutine。

  4. 调度器:Go 语言的运行时系统内置了一个高效的调度器(Scheduler),负责管理和调度所有 Goroutines 的执行。调度器可以根据系统资源(如 CPU 核心数)和 Goroutines 的运行状态动态调整其调度策略,确保并发任务高效、公平地执行。

  5. 通信代替共享内存:Go 语言鼓励通过 channels(通道)进行 Goroutines 之间的通信和同步,遵循"不要通过共享内存来通信,而应通过通信来共享内存"的原则,避免了传统并发编程中常见的数据竞争和竞态条件问题。

  6. 栈动态增长:每个 Goroutine 初始时分配一个小栈(通常为几 KB),随着函数调用深度的增加,栈空间不足时会自动增长(可达几 MB),避免了预估栈大小的难题,同时也减少了内存浪费。

综上所述,Goroutine 是 Go 语言中实现并发编程的关键特性,它是一种轻量级、协程化的执行单元,由 Go 语言运行时系统原生支持并高效调度。通过 Goroutines,开发者可以方便、高效地编写并发程序,利用多核处理器能力,同时借助 channels 和其他同步原语确保数据安全和正确性。

2.GMP 指的是什么

  • G( Goroutine): 我们所说的协程,为用户级的轻量级线程,每个Goroutine对象中的sched保存着其上下文信息。
  • M( Machine): 对内核级线程的封装,数量对应真实的 CPU 数(真正干活的对象)。
  • P( Processor): 即为 G 和 M 的调度对象,用来调度 G 和 M 之间的关联关系,其数量可通过 GOMAXPROCS()来设置,默认为核心数。

3.GMP模型的简介

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

全局队列(Global Queue)

存放等待运行的G。

P的本地队列

  • 同全局队列类似,存放的也是等待运行的G
  • 存的数量有限,不超过256个。
  • 新建G'时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。

P列表

所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个。

M列表

  • 当前操作系统分配到当前Go程序的内核线程数
  • 线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。
  • M运行G,G执行之后,M会从P获取下一个G,不断重复下去。
    Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行。

4.有关P和M的个数问题

P的数量问题

由启动时环境变量$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()决定。这意味着在程序执行的任意时刻都只有$GOMAXPROCS个goroutine在同时运行。

M的数量问题

  • go语言本身的限制:go程序启动时,会设置M的最大数量,默认10000.但是内核很难支持这么多的线程数,所以这个限制可以忽略。
  • runtime/debug中的SetMaxThreads函数,设置M的最大数量
  • 一个M阻塞了,会创建新的M。

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

P和M何时会被创建

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

5.调度器P的设计策略

复⽤线程

避免频繁的创建、销毁线程,⽽是对线程的复⽤。

work stealing机制

当本线程⽆可运⾏的G时,尝试从其他线程绑定的P偷取G,⽽不是销毁线程。

hand off机制

当本线程因为G进⾏系统调⽤阻塞时,线程释放绑定的P,把P转 移给其他空闲的线程执⾏。

利⽤并⾏

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

抢占

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

全局G队列

在新的调度器中依然有全局G队列,当P的本地队列为空时,优先从全局队列获取,如果全局队列为空时则通过work stealing机制从其他P的本地队列偷取G。

6."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会被放入全局队列中。

7.调度器的生命周期

M0

M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G, 在之后M0就和其他的M一样了。

G0

是每次启动一个M都会第一个创建的gourtine,G0仅用于负责调度的G,G0不指向任何可执行的函数, 每个M都会有一个自己的G0。在调度或系统调用时会使用G0的栈空间, 全局变量的G0是M0的G0。

我们来跟踪一段代码

Go 复制代码
package main

import "fmt"

func main() {
    fmt.Println("Hello world")
}
  1. runtime创建最初的线程m0和goroutine g0,并把2者关联。
  2. 调度器初始化:初始化m0、栈、垃圾回收,以及创建和初始化由GOMAXPROCS个P构成的P列表。
  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结束而结束。

8.Sysmon有什么作用

sysmon是一个管理线程或者说守护线程,其是对GMP调度架构的补充和兜底。

GMP的调度完全是主动协作式的调度。主动协作式的调度性能很高,但是在某些情况下会出现单个goroutine长期占据时间片甚至一直占据时间片的情况。

比如:

  1. 某个goroutine不执行主动调度、不调用系统调用、不做函数调用,就会一直运行直到goroutine退出;
  2. 某个goroutine处于syscall状态时也无法触发主动调度,可能会造成该goroutine长时间占据时间片;

sysmon的作用就是处理类似上面情况,其主要的工作内容有:

  1. 定期查看netpoll有无就绪的任务,防止netpoll阻塞队列中的goroutine饥饿;
  2. 定期查看是否有p长时间(10ms)处于syscall状态,如有则将p的持有权释放以执行其他g;
  3. 定期查看是否有p长时间(10ms)没有调度,如有则对当前m发送信号,触发基于信号的异步抢占调度;

9.可视化GMP编程

方式1:go tool trace

方式2:Debug trace

参考:2、Golang的协程调度器原理及GMP设计思想 · 语雀

相关推荐
blammmp21 分钟前
Java:数据结构-枚举
java·开发语言·数据结构
何曾参静谧34 分钟前
「C/C++」C/C++ 指针篇 之 指针运算
c语言·开发语言·c++
暗黑起源喵39 分钟前
设计模式-工厂设计模式
java·开发语言·设计模式
WaaTong44 分钟前
Java反射
java·开发语言·反射
Troc_wangpeng1 小时前
R language 关于二维平面直角坐标系的制作
开发语言·机器学习
努力的家伙是不讨厌的1 小时前
解析json导出csv或者直接入库
开发语言·python·json
Envyᥫᩣ1 小时前
C#语言:从入门到精通
开发语言·c#
齐 飞1 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
童先生1 小时前
Go 项目中实现类似 Java Shiro 的权限控制中间件?
开发语言·go
lulu_gh_yu1 小时前
数据结构之排序补充
c语言·开发语言·数据结构·c++·学习·算法·排序算法