I'm spiderman I'm spiderman
首页
  • 中间件
  • 基础架构
  • 微服务
  • 云原生
  • Java
  • Go
  • PHP
  • Python
  • 计算机网络
  • 操作系统
  • 数据结构
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档

spiderman

快乐学习,快乐编程
首页
  • 中间件
  • 基础架构
  • 微服务
  • 云原生
  • Java
  • Go
  • PHP
  • Python
  • 计算机网络
  • 操作系统
  • 数据结构
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
  • Java

  • Golang

    • golang基本原理
    • golang调度原理
      • 二.golang对协程的处理
        • 协程和goroutine关系
        • Go的GMP调度模型
        • 调度器的设计策略
        • go func()调度流程
        • 调度器的生命周期
  • PHP

  • Python

  • 计算机语言
  • Golang
spiderman
2023-05-03
目录

golang调度原理

# 一.golang 调度器

多线程/多进程操作系统

并发执行进程/线程时

进程/线程数量越多,切换成本就越大 多线程伴随着同步竞争(锁、资源冲突等) golang-1

存在问题:

  1. 高内存占用

  2. 调度的高消耗CPU(上下文切换) golang-2

后来人们发现一个线程可被分为用户态和内核态线程

用户态线程对CPU透明

一个用户态线程绑定内核态线程(Linux 的 PCB 进程控制块),这样用户态任务可以切换,而CPU执行的线程不变,减少了切换线程带来的开销

约定:

    内核线程 = 线程

    用户线程 = 协程

线程由CPU调度,是抢占式的

协程由用户态调度,是协作式的(一个协程让出CPU后,才执行下一个协程)

goroutine由runtime(go协程调度器)进行管理,而不是操作系统 golang-3

一个/多个协程可绑定在一个/多个线程上

3种协程和线程的关系

  1. N:1关系——N个协程绑定1个线程

golang-4

优点:

    协程在用户态线程即完成切换,不会陷入到内核态,切换轻量快速

缺点:

    一个进程的所有协程都绑定在一个线程上,一旦某个协程阻塞,协程调度器没有切换协程的能力时会造成线程阻塞,本进程的其他协程都无法执行了,失去并发能力
  1. 1:1关系——1个协程绑定1个线程 golang-5

优点:

    实现简单,协程的调度由CPU完成,不存在N:1的缺点

缺点:

    协程的创建、删除和切换的代价都由CPU完成,略显昂贵
  1. M:N关系——M个协程绑定1个线程 golang-6

优点:

    克服了前两种的缺点

缺点:

    实现起来最复杂

# 二.golang对协程的处理

# 协程和goroutine关系

    Go 为了提供更容易使用的并发方法,使用了 goroutine 和 channel。

goroutine

    来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被runtime调度

# Go的GMP调度模型

golang-7

GMP是Go运行时调度层面的实现,负责在适当实际将合适的协程分配到合适的位置,保证公平和效率

包含4个重要结构,分别是G、M、P、Sched

全局队列

    存放等待运行的G

P的本地队列

    存放等待运行的G

    数量限制,不能超过256G

    优先将新创建的G放在P的本地队列中

G 协程

    是Goroutine的缩写

    相当于操作系统的进程控制块(process control block)。

    它包含:函数执行的指令和参数,任务对象,线程上下文切换,字段保护,和字段的寄存器。

M 线程

    每个M都有一个线程的栈。如果没有给线程的栈分配内存,操作系统会给线程的栈分配默认的内存。当线程的栈制定,M.stack->G.stack, M的PC寄存器会执行G提供的函数。

P (处理器,Processor)

    包含运行goroutine的资源

    如果线程想运行goroutine,必须先获取P。P中还包含了可运行的G队列。

    优先将新创建的G放在P的本地队列中,如果满了会放在全局队列中

P 和 M 何时会被创建

P 何时创建:

    在确定了 P 的最大数量 n 后,运行时系统会根据这个数量创建 n 个 P。

M 何时创建:

    没有足够的 M 来关联 P 并运行其中的可运行的 G。比如所有的 M 此时都阻塞住了,而 P 中还有很多就绪任务,就会去寻找空闲的 M,而没有空闲的,就会去创建新的 M。

golang-8

P和M的个数 P的个数——在同一时刻的P个数,而不是宏观的并发

    程序启动时创建

    最多有GOMAXPROCS个

配置方法:

  1. 环境变量$GOMAXPROCS个(可配置)

  2. 在程序中通过runtime.GOMAXPROCS()来设置

M的个数——动态数量

    动态的,一旦有一个M阻塞,就会创建一个新的M

    如果有M空闲,就会回收/睡眠

     Go语言本身,限定了M的最大量是10000(忽略)

    通过runtime/debug包中的SetMaxThreads函数来设置

# 调度器的设计策略

(一)复用线程 避免频繁的创建、销毁线程,而是对线程的复用

  1. work stealing机制

     当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程
    
  2. hand off机制

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

(二)利用并行 eg.GOMAXPROCS限定P的个数,最多有GOMAXPROCS个线程分布在多个CPU上同时运行

(三)抢占 之前:coroutine,要等待另一个协程主动释放CPU才执行下一个协程

现在:一个goroutine最多占用CPU10ms时间片轮询),防止其他goroutine被饿死(抢占)

(四)全局G队列 基于work stealing机制的补充,当M从其他P偷不到G时,他可以从全局G队列中获取G

# go func()调度流程

golang-9

go func()

  1. 创建一个G

  2. 1G优先放到当前线程持有的P的本地队列中;

  3. 2如果已经满了,则放入全局队列中

  4. 1M通过P获取G;(一个M必须持有一个P——1:1)

  5. 2如果M的本地队列为空,从全局队列获取G;

  6. 3(work stealing机制)如果也为空,则从其他的MP组合偷取G

  7. 调度

  8. 执行func()函数

5.1 超出时间片后返回P的本地队列

5.2 若G.func()发生systemCall/阻塞

5.2.1 runtime(即调度器)会把这个M从P中摘除,(hand off机制)创建一个M或从休眠队列中取一个空闲的M,接管正在被阻塞中的P

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

6.销毁G

7.返回

# 调度器的生命周期

M0和G0

M0(进程唯一)

启动程序后的编号为0的主线程

在全局变量runtime.m0中,不需要在heap上分配

负责执行初始化操作和启动第一个G

启动第一个G后,M0就和其他的一样了

G0(线程唯一)

每启动一个M,都会第一个创建G0,每个M都会有一个自己的G0

G0不指向任何可执行的函数

G0仅用于负责调度G,在调度/系统调用时,M会切换到G0,调度其他G

M0的G0会放在全局空间

package main
 
import "fmt"
 
func main() {
    fmt.Println("Hello world")
}
1
2
3
4
5
6
7
  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 结束而结束。 golang-10
#golang
golang基本原理
安装php环境

← golang基本原理 安装php环境→

最近更新
01
innovation create future
12-13
02
RabbitMQ
12-06
03
StarRocks的应用
09-11
更多文章>
Theme by Vdoing | Copyright © 2022-2024 spiderman | 粤ICP备2023019992号-1 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式