Go语言设计与实现 -- 调度器总体概述

news2025/1/13 15:42:30

Go语言调度器使用与CPU数量相等的线程来减少线程频繁切换带来的内存开销,同时在每一个线程上执行额外开销更低的Goroutine来降低操作系统和硬件的负载。

在这里插入图片描述

每一次线程上下文切换都需要消耗约1us的时间,而Go调度器对Goroutine的上下文的切换约为0.2us,减少了80%的额外开销!

演变过程

我们先来了解一下Goroutine的演变过程。

  • 单线程调度器 – Go 0.X

    程序中只能存在一个活跃线程。由G - M模型组成

  • 多线程调度器 – Go 1.0

    全局锁导致竞争严重

  • 任务窃取调度器 – Go 1.1

    改进点:

    • 引入了处理器P,构成了目前的G-M-P模型
    • 在处理器P的基础上实现了基于工作窃取的调度器

    缺陷点:

    • 在某些情况下Goroutine不会让出线程,进而导致饥饿问题
    • 垃圾收集机制时间过长
  • 抢占式调度 – Go 1.2至今

    • 基于协作的抢占式调度器 – Go 1.2 ~ Go 1.13

      改进:

      • 通过编译器在函数调用时插入抢占检查指令,在函数调用时检查当前Goroutine是否发起了抢占请求,实现基于协作的抢占式调度

      缺陷:

      • Goroutine可能会因为垃圾收集和循环长时间占用资源导致程序暂停
    • 基于信号的抢占式调度器 – Go 1.14 ~ 至今

      改进:

      • 实现了基于信号的真抢占式调度

      缺陷:

      • 垃圾收集在扫描线程时会触发抢占式调度
      • 抢占的时间点不够多,不能覆盖所有边缘情况

布局

golang-scheduler

该图来自面向信仰编程

  • G表示Goroutine,它是待执行的任务
  • M表示操作系统的线程,它由操作系统的调度器调度和管理
  • P表示处理器,可以把它看作在线程上运行的本地调度器

我们必须有一个整体的认识才能开始学习:

img

G

数据结构

Goroutine只存在于Go的运行时,它是Go在用户态提供的线程。G的结构体有40多个字段,很难全部弄清楚,所以我们来挑选几个经典的来进行讲解:

type g struct {
    goid    int64 // 唯一的goroutine的ID
    sched gobuf // goroutine切换时,用于保存g的上下文
    stack stack // 栈
  	gopc        // pc of go statement that created this goroutine
    startpc    uintptr // pc of goroutine function
    ...
}

type gobuf struct {
    sp   uintptr // 栈指针位置
    pc   uintptr // 运行到的程序位置
    g    guintptr // 指向 goroutine
    ret  uintptr  // 保存系统调用的返回值
    ...
}

//[stack.lo, stack.hi)
type stack struct {
    lo uintptr // 栈的下界内存地址
    hi uintptr // 栈的上界内存地址
}

Goroutine的状态

状态含义
空闲中_GidleG刚刚新建,没有初始化
待运行_Grunnable就绪状态,G在运行队列中,等待M取出并运行
运行中_GrunningM正在运行这个G,这时候M会拥有一个P
系统调用中_GsyscallM正在运行这个G发起的系统调用,这时候M并不拥有P
等待中_GwaitingG在等待某些条件完成,这时候G不在运行也不在运行队列中(可能在channel的等待队列中)
已中止_GdeadG未被使用,可能已经执行完毕
栈复制中_GcopystackG正在获取一个新的栈空间并把原来的内容复制过去(用于防止GC扫描)

img

M

M是操作系统线程,调度器最多可以创建10 000个线程,但是绝大多数线程不会执行用户的代码,最多只会有GOMAXPROCS个活跃线程能够运行。默认情况下GOMAXPROCS会设置成当前机器的核数。

type m struct {
    g0            *g     
    // 每个M都有一个自己的G0,不指向任何可执行的函数,在调度或系统调用时,M会切换到G0,使用G0的栈空间来调度
    curg          *g    
    // 当前正在执行的G
    ... 
}
  • g0是持有调度栈的Goroutine,它会深度参与运行时的调度过程,包括Goroutine的创建,大内存分配核cgo函数的执行
  • curg是在当前线程上运行的用户的Goroutine

P

调度器中的处理器P是线程核Goroutine的中间层,它能提供线程需要的上下文,也会负责调度线程上的等待队列。通过处理器P的调度,每一个内核线程都能执行多个Goroutine,它能在Goroutine进行一些I/O操作时及时让出计算资源,提供线程利用率。

有多少个活跃线程就有多少个P。

数据结构

P中有非常多字段,例如性能追踪,垃圾收集,计时器相关等,但是这里暂时先讲解线程和运行队列。

type p struct {
    lock mutex
    id          int32
    status      uint32 // one of pidle/prunning/...

    // Queue of runnable goroutines. Accessed without lock.
    runqhead uint32 // 本地队列队头
    runqtail uint32 // 本地队列队尾
    runq     [256]guintptr // 本地队列,大小256的数组,数组往往会被都读入到缓存中,对缓存友好,效率较高
    runnext guintptr // 下一个优先执行的goroutine(一定是最后生产出来的),为了实现局部性原理,runnext中的G永远会被最先调度执行
    ... 
}

处理器状态

状态描述
_Pidle处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空
_Prunning被线程M持有,并且正在执行用户代码或者调度器
_Psyscall没有执行用户代码,当前线程陷入系统调用(例如去执行I/O操作了)
_Pgcstop被线程M持有,当前处理器由于垃圾收集而被停止
_Pdead当前处理器已经被停用了

Go调度器实现原理

调度器启动

我们先宏观的启动整个调度器,之后会等待用户创建,运行新的Goroutine并为Goroutine调度处理器资源。

创建Goroutine

调用runtime.newproc获取新的Goroutine结构体将其加入处理器的运行队列,并在满足条件时调用runtime.wakep唤醒新的处理器执行Goroutine。下面来详细的展开讲解这一句话:

初始化结构体

golang-newproc-get-goroutine

先总结一下:runtime.newproc1会从处理器或者调度器的缓存中获取新的结构体,也可以调用runtime.malg函数创建。

这个逻辑由runtime.gfget这个函数来实现:

  • Goroutine所在处理器的gFree列表或者调度器的sched.gFree列表中获取runtime.g
  • 调用runtime.malg生成一个新的runtime.g并将结构体追加到全局的Goroutine列表allgs

如果调度器和处理器的gFree列表都不存在结构体时,才会新创建,否则会根据处理器中gFree列表中的Goroutine数量做出不同的决策:

  • 处理器的Goroutine列表对象为空时,会将调度器持有的空闲Goroutine转移到当前处理器上,知道gFree列表中的Goroutine数量达到32
  • 处理器的Goroutine数量充足时,会从列表头部返回一个新的Goroutine

运行队列

runtime.runqput会将Goroutine放到运行队列上,这既可能是全局的运行队列,也可能是处理器的本地队列。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pchCgpgq-1672989679776)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20230106001757467.png)]

这张图我们会随着讲解逐渐展开。

调度信息

我们前面有讲过一个结构体:

type gobuf struct {
    sp   uintptr // 栈指针位置
    pc   uintptr // 运行到的程序位置
    g    guintptr // 指向 goroutine
    ret  uintptr  // 保存系统调用的返回值
    ...
}

这个结构体保存Goroutine的上下文信息,也就是调度信息,其中sp存储了runtime.goexit函数的程序计数器,pc中存储了传入函数的程序计数器,pc寄存器的作用是存储程序接下来要运行的位置,所以pc的使用比较好理解,但是sp中存储的goexit不好理解,需要结合下面的调度循环来进行理解。

调度循环

我们先来看一张图宏观的理解一下什么叫做调度循环

golang-scheduler-loop

使用什么策略来挑选下一个Goroutine执行。由于P中的G分布在runnext,本地队列,全局队列,网络轮询器中,则需要挨个判断是否有可执行的G,大体逻辑如下:

  • 每执行61次调度循环,从全局队列获取G,若有则直接返回
  • 从P的runnext看一下是否有G,若有则直接返回
  • 从P上的本地队列看一下是否有G,若有则直接返回
  • 上面都没有查到时,则取全局队列,网络轮询器查找或者从其他P中窃取,一直阻塞直到获取到一个可用的G为止

首先是调用schedule函数。

func schedule() {
    _g_ := getg()
    var gp *g
    var inheritTime bool
    ...
    if gp == nil {
        // 每执行61次调度循环会看一下全局队列。为了保证公平,避免全局队列一直无法得到执行的情况,当全局运行队列中有待执行的G时,通过schedtick保证有一定几率会从全局的运行队列中查找对应的Goroutine;
        if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
            lock(&sched.lock)
            gp = globrunqget(_g_.m.p.ptr(), 1)
            unlock(&sched.lock)
        }
    }
    if gp == nil {
        // 先尝试从P的runnext和本地队列查找G
        gp, inheritTime = runqget(_g_.m.p.ptr())
    }
    if gp == nil {
        // 仍找不到,去全局队列中查找。还找不到,要去网络轮询器中查找是否有G等待运行;仍找不到,则尝试从其他P中窃取G来执行。
        gp, inheritTime = findrunnable() // blocks until work is available
        // 这个函数是阻塞的,执行到这里一定会获取到一个可执行的G
    }
    ...
    // 调用execute,继续调度循环
    execute(gp, inheritTime)
}

然后就的调度循环的execute函数,执行获取的Goroutine,做好工作后使用gogoGoroutine调度到当前线程上。在gogo函数中,经过一系列复杂的函数调用,最终会调用goexit0函数,将Goroutine转换为_Gdead状态,清除其中的字段,移除Goroutine和线程的关联,并调用gfput重新加入处理器的Goroutine的空闲列表gFree。最后goexit0函数重新调用schedule开始新一轮的循环,不出现意外的话循环是永不终止的。

触发调度

下面简单介绍所有触发调度的时间点:

  • 主动挂起 — runtime.gopark -> runtime.park_m
  • 系统调用 — runtime.exitsyscall -> runtime.exitsyscall0
  • 协作式调度 — runtime.Gosched -> runtime.gosched_m -> runtime.goschedImpl
  • 系统监控 — runtime.sysmon -> runtime.retake -> runtime.preemptone

线程管理

Go运行时会通过调度器改变线程的所有权,绑定Goroutine和当前线程。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

01_FreeRTOS基础知识

目录 裸机与RTOS介绍 裸机与RTOS特点 FreeRtos简介 任务调度简介 抢占式调度 时间片调度 协程式调度 任务状态 裸机与RTOS介绍 假设小明在打游戏,此时女盆友微信回复了信息。 裸机:在裸机上实现是等这游戏打完之后,在去回复女朋友的信息,假设游戏刚刚开始打完需要半小…

使用缓存保护MySQL

1 更新缓存最佳实践 Redis的执行器非常薄,所以Redis只支持有限API,几乎没聚合查询能力,也不支持SQL。存储引擎也简单,直接在内存中用最简单数据结构保存数据。 如Redis的LIST在存储引擎的内存中的数据结构就是双向链表。内存是易…

基于ONNX人脸识别实例(SCRFD/ArcFace)-C#版

一、引用 Microsoft.ML.OnnxRuntime OpenCvSharp OpenCvSharp.Extensions二、人脸检测(Face Detection) using System; using System.Collections.Generic; using System.Linq; using Microsoft.ML.OnnxRuntime; using Microsoft.ML.OnnxRuntime.Tensors; using OpenCvSharp;…

c++11 标准模板(STL)(std::deque)(十)

定义于头文件 <deque> std::deque 修改器 移除首元素 std::deque<T,Allocator>::pop_front void pop_front(); 移除容器首元素。若容器中无元素&#xff0c;则行为未定义。 指向被擦除元素的迭代器和引用被非法化。若元素是容器中的最后元素&#xff0c;则尾后…

2022 数字IC设计秋招复盘——数十家公司笔试题、面试实录

0 引言 秋招结束了。 “今年是前五年最差的一年&#xff0c;也将是后五年最好的一年”&#xff0c;虽然无法预知后面的就业情况&#xff0c;但就我今年自己的亲身经历与去年师兄师姐找工作的情况对比&#xff0c;感觉难度确实是增大了很多。我总共投递了80家左右的公司&#…

德云社相声春晚未播先火,郭德纲独揽三个节目,四位老艺术家助阵

随着央视春晚的二次彩排&#xff0c;德云社相声春晚&#xff0c;也被安排到议事日程当中&#xff0c;听说今年的相声春晚还颇有看点。由于缺少了岳云鹏张云雷等得力干将&#xff0c;郭德纲老师决定亲自下场&#xff0c;一个人就独揽了三个节目。 按说德云社举办相声春晚&#x…

乒乓普及套及廉价底板评测

疫情的末端期间开始打乒乓球&#xff0c;最开始在单位打&#xff0c;后来去了花园和大爷们打。用了几个拍子和胶皮&#xff0c;都是网上最便宜的&#xff0c;现在在野球场能排到前十吧&#xff0c;我打球比较“正”&#xff08;他人评价&#xff09;&#xff0c;大家比较愿意和…

基于Java+SpringBoot+vue+element等动物救助平台设计和实现

基于JavaSpringBootvueelement等动物救助平台设计和实现 博主介绍&#xff1a;5年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 超级帅帅吴 Java毕设项目精品实战案例《500套》 欢迎点赞 收藏 ⭐留言 文末获取…

数字人民币创新浪潮来袭,支付机构如何“乘风破浪”?

易观&#xff1a;近年来&#xff0c;数字经济浪潮迭起&#xff0c;数字科技方兴未艾。法定数字货币作为各国政府掌握货币发行控制权的重要载体&#xff0c;正在全球范围内全面铺开。法定数字货币的推出将大幅提升货币的流转效率&#xff0c;为央行管理职能赋予了数字化内涵。可…

Golang 常用字符串函数

统计字符串长度&#xff0c;按字节 len(str)str : "你好" fmt.Println("len", len(str))字符串遍历&#xff0c;同时处理有中文的问题 s : []rune(str)str : "你好" s : []rune(str) for i : 0; i < len(s); i {fmt.Printf("string%c\n&…

安全知识答题小程序v2.0与v3.0的异同点一览

安全知识答题小程序安全知识答题小程序这个软件架构是微信原生小程序云开发。主要包含六大功能模块页面&#xff0c;首页、答题页、结果页、活动规则页、答题记录页、排行榜页。v2.0的功能有以下&#xff1a;排行榜页答题记录页活动规则页微信授权登录获取微信头像和昵称等首页…

SQL SELECT TOP, LIMIT, ROWNUM 子句

SQL SELECT TOP 子句 SELECT TOP 子句用于规定要返回的记录的数目。 SELECT TOP 子句对于拥有数千条记录的大型表来说&#xff0c;是非常有用的。 注意:并非所有的数据库系统都支持 SELECT TOP 语句。 MySQL 支持 LIMIT 语句来选取指定的条数数据&#xff0c; Oracle 可以使用…

java后端第五阶段:Git

一、开发场景 备份、代码还原、协同开发、追溯问题代码的编写人和编写时间&#xff01; 安装&#xff1a;直接去官网下载&#xff0c;傻瓜式安装 二、Git常用指令 1.设置用户签名 git config --global user.name 用户名 git config --global user.email 邮箱 2.初始化本地仓…

用存储过程、定时器、触发器来解决数据分析问题

做数据分析或者数据处理&#xff0c;我们也需要掌握这些技能&#xff0c;来解决特定的业务问题。比如&#xff1a;做自动化报表&#xff0c;如果数据需要每天实时更新&#xff08;增量爬虫&#xff09;、定时计算某个业务指标 、想要实时监控数据库表中的数据增、删、改情况等。…

四万字总结Redis语法、配置、实战

文章目录一、安装1.Linux下安装下载解压安装修改配置设置环境变量启动、连接查看redis进程退出2.windows下安装下载并解压二、系统管理1.常用key相关的命令2.时间相关命令3.设置相关命令CONFIG GET & CONFIG SET4.查询信息5.密码设置三、基本数据类型1.Redis strings2.Redi…

Golang常用结构源码01-Map

Golang集合源码-Map 22届211本科&#xff0c;在字节实习了一年多&#xff0c;正式工作了半年&#xff0c;工作主要是golang业务研发&#xff0c;go语言使用和生态比java简洁很多&#xff0c;也存在部分容易遇见的问题&#xff0c;常用结构需要对其底层实现略有了解才不容易写出…

小程序学习笔记

注册小程序账号 www.mp.weixin.qq.com 获取appid 微信开发者工具下载 https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html 设置代理和外观 创建小程序项目 小程序项目结构 app.json文件 这个文件的第一个路径就是小程序的默认页面 window配置项 …

使用kettle同步全量数据到Elasticsearch(es)--elasticsearch-bulk-insert-plugin应用

背景 为了前端更快地进行数据检索&#xff0c;需要将数据存储到es中是一个很不错的选择。由于公司etl主要工具是kettle&#xff0c;这里介绍如何基于kettle的elasticsearch-bulk-insert-plugin插件将数据导入es。在实施过程中会遇到一些坑&#xff0c;这里记录解决方案。 可能…

Java集合类ArrayList应用 | 二维数组的集合类表示与杨辉三角实现

目录 一、题干 &#x1f517;力扣&#xff1a;118. 杨辉三角 二、题解 1. 思路 2. 完整代码 三、总结 一、题干 &#x1f517;力扣&#xff1a;118. 杨辉三角 二、题解 1. 思路 我们知道杨辉三角的规律是&#xff1a; 每一行的第一列和它的最后一列上的数均为1.除此之…

如何在实验室服务器上跑代码

1.工具准备 可以下载一个xshell或secureCRT或者其他shell工具&#xff0c;通过ssh方式连接服务器&#xff0c;然后通过本地电脑终端控制服务器。连接方式输入主机&#xff08;Host&#xff09;,和端口号&#xff08;一般是22&#xff09;就行了。如下图 连接成功后就可以在本…