Go并发调度-调度器设计理念从何而来?为何如此高效?

深入浅出 Go 语言的 GPM 模型(Go1.21) 本文基于 Go1.21.0 版本详细介绍了 Go 语言的 GPM 模型。 阅读详情

图片

Go并发调度进阶-【公粽号:堆栈future】

1. 调度器的基本设计原则和概念

我们首先了解一下调度器的设计原则及一些基本概念来建立对调度器较为宏观的认识。理解调度器涉及的主要概念包括以下三个:

  • G: Goroutine,即我们在 Go 程序中使用 go 关键字创建的执行体;

  • M: Machine,或 worker thread,即传统意义上进程的线程;

  • P: Processor,即一种人为抽象的、用于执行 Go 代码的处理器。只有当 M 与一个 P 关联后才能执行 Go 代码。除非 M 发生阻塞或在进行系统调用时间过长时,没有与之关联的 P。

1. M工作线程的暂止和复始

运行时调度器的任务是给不同的工作线程 (worker thread) 分发可供运行的(ready-to-run)Goroutine。我们不妨设每个工作线程总是贪心的执行所有存在的 Goroutine,那么当运行进程中存在 n 个线程(M),且 每个 M 在某个时刻有且只能调度一个 G。

那么会有以下性质:

  • 性质 1:当用户态代码创建了p(p>n)个G时,则必定存在p−n个 G 尚未被 M 调度执行;

  • 性质 2:当用户态代码创建的q(q<n)时,则必定存在 n-q 个 M 不存在正在调度的 G。

这两条性质分别决定了工作线程的 暂止(park) 和 复始(unpark),复始就是解除暂止。

我们不难发现,调度器的设计需要在性质1和性质2之间进行权衡:即既要保持足够的运行工作线程来利用有效硬件并发资源,又要暂止过多的工作线程来节约 CPU 能耗。如果我们把调度器想象成一个系统,则寻找这个权衡的最优解意味着我们必须求解调度器系统中 每个 M 的状态,即系统的全局状态。这是非常困难的,不妨考虑以下两个难点:

难点 1: 在多个 M 之间不使用屏障的情况下,得出调度器中多个 M 的全局状态是不可能的。

我们都知道计算的局部性原理,为了利用这一原理,调度器所需调度的 G 都会被放在每个 M 自身对应的本地队列中。换句话说,每个 M 都无法直接观察到其他的 M 所具有的 G 的状态,存在多个 M 之间的共识问题。这本质上就是一个分布式系统。显然,每个 M 都能够连续的获取自身的状态,但当它需要获取整个系统的全局状态时却不容易, 原因在于我们没有一个能够让所有线程都同步的时钟。换句话说, 我们需要依赖屏障来保证多个 M 之间的全局状态同步。更进一步,在不使用屏障的情况下, 能否利用每个 M 在不同时间中记录的本地状态中计算出调度器的全局状态呢,答案是不可能的。

难点 2: 为了获得最佳的线程管理,我们必须得获得未来的信息,即当一个新的 G 即将就绪(ready)时,则不再停止一个工作线程。

举例来说,目前我们的调度器存在 4 个 M,并其中有 3 个 M 正在调度 G,则其中有 1 个 M 处于空闲状态。这时为了节约 CPU 能耗,我们希望对这个空闲的 M 进行暂止操作。但是,正当我们完成了对此 M 的暂止操作后, 用户态代码正好执行到了需要调度一个新的 G 时,我们又不得不将刚刚暂止的 M 重新启动,这无疑增加了开销。我们当然有理由希望,如果我们能知晓一个程序生命周期中所有的调度信息, 提前知晓什么时候适合对 M 进行暂止自然再好不过了。尽管我们能够对程序代码进行静态分析,但这显然是不可能的:考虑一个简单的 Web 服务端程序,每个用户请求 到达后会创建一个新的 G 交于调度器进行调度。但请求到达是一个随机过程,我们只能预测可能到达的请求数,而不能完整知晓所有的调度需求。

那么我们又应该如何设计一个通用且可扩展的调度器呢?我们很容易想到三种平凡的做法:

设计 1: 集中式管理所有状态

显然这种做法自然是不可取的,在多个并发实体之间集中管理所有状态这一共享资源,需要锁的支持, 当并发实体的数量增大时,将限制调度器的可扩展性。

设计 2: 每当需要就绪一个 G1 时,都让出一个 P,直接切换出 G2,再复始一个 M 来执行 G2

因为复始的 M 可能在下一个瞬间又没有调度任务,则会发生线程颠簸(thrashing),进而我们又需要暂止这个线程。另一方面,我们希望在相同的线程内保存维护 G,这种方式还会破坏计算的局部性原理。

设计 3: 任何时候当就绪一个 G、也存在一个空闲的 P 时,都复始一个额外的线程,不进行切换

因为这个额外线程会在没有检查任何工作的情况下立即进行暂止,最终导致大量 M 的暂止和复始行为,产生大量开销。

基于以上考虑,目前的 Go 的调度器实现中设计了工作线程的自旋(spinning)状态,放弃了上述三种设计:

  1. 如果一个工作线程的本地队列、全局运行队列或网络轮询器(netpoller)中均没有可调度的任务,则该线程成为自旋线程;

  2. 满足该条件、被复始的线程也被称为自旋线程,对于这种线程,运行时不做任何事情。

自旋线程在进行暂止之前,会尝试从任务队列中寻找任务。当发现任务时,则会切换成非自旋状态, 开始执行 Goroutine。而找到不到任务时,则进行暂止。

当一个 Goroutine 准备就绪时,会首先检查自旋线程的数量,而不是去复始一个新的线程。

如果最后一个自旋线程发现了任务并且停止自旋时,则复始一个新的自旋线程。这个方法消除了不合理的线程复始峰值,且同时保证最终的最大 CPU 并行度利用率。

我们可以通过下图来直观理解工作线程的状态转换:

  如果存在空闲的 P,且存在暂止的 M,并就绪 G
          +------+
          v      |
执行 --> 自旋 --> 暂止
 ^        | 自旋期间没有任务就将M暂止
 +--------+
  如果发现task就执行 执行完成就自旋

总的来说,调度器的方式可以概括为:如果存在一个空闲的 P 并且没有自旋状态的工作线程 M,则当就绪一个 G 时,就复始一个额外的线程 M。这个方法消除了不合理的线程复始峰值,且同时保证最终的最大 CPU 并行度利用率。

因此,就绪(ready)一个 G 的通用流程为:

  • 提交一个 G 到 per-P 的本地工作队列 // 后续文章探讨per-P

  • 执行 StoreLoad 风格的写屏障 // x86架构的写屏障:

  • 检查 sched.nmspinning 数量 // nm:n个M自旋的数量

而从自旋到非自旋转换的一般流程为:

  • 减少 nmspinning 的数量

  • 执行 StoreLoad 风格的写屏障

  • 在所有 per-P 本地任务队列检查新的任务

当然,此种复杂性在全局任务队列中并不适用,因为当给一个全局队列提交任务时, 不进行线程的复始操作。

下篇从源码角度来分析下调度流程,感受不一样的底层设计魅力。

公粽号:堆栈future

使很多处于迷茫阶段的coder能从这里找到光明,堆栈创世,功在当代,利在千秋

123篇原创内容

图片

Golang领域go routine调度器的设计理念与实现 本文旨在全面解析Golang runtime中goroutine调度器的设计理念和实现细节。我们将深入探讨调度器的架构、工作原理以及性能优化策略,帮助开发者更好地理解和使用Golang的并发特性。文章首先介绍Golang并发模型的基本概念,然后深入分析GMP调度模型的设计理念,接着详细讲解调度器的实现机制,最后探讨实际应用中的优化策略和未来发展方向。Goroutine: Golang中的轻量级线程,由Go运行时管理GMP模型。 阅读详情

相关推荐

Go-Spaceship:构建多人游戏后端框架的Golang实践

Go-Spaceship后端框架创立的初衷是为了提供一种高并发、易扩展、快速开发的后端服务解决方案。随着互联网技术的发展,对于后端服务的性能要求越来越高,传统的框架很难满足日益增长的性能和开发效率需求。因此,Go-Spaceship应运而生,旨在为开发者提供一个简洁、高效、可扩展的开发环境。Go-Spaceship框架将应用分解为多个模块化的组件,每个组件都有明确的职责范围。比如,框架可能包含以下几个核心组件:路由组件(Router):负责处理HTTP请求,并将其分发到对应的处理函数。

weixin_33628677的博客 1246

如何自己设计一个定时任务分布式调度器

使用分布式调度器能够很好的管理我们的定时任务接口,开发人员也只需专注开发业务接口,让业务与配置完全分离。定时配置还可以根据业务场景统一进行时间协调管理,以免在有些时间点多任务同时处理,可以将时间配置的分散点以减轻CPU的压力。如果系统业务量少,定时任务也不多的情况也没必要多浪费时间开发一个调度系统。

jcc4261的博客 342

实时操作系统软件调度器/硬件调度器的设计与实现

本文讨论了三种任务调度的实现方法,分别为:软件调度器模型、协处理器调度模型、硬件调度器模型,并对其性能进行了测试。

手把手教你设计一个任务调度器

我们从业务现状出发,考虑需要覆盖的场景,设计调度器需要提供的角色。然后确定了调度器的关键接口,同时给出了简单实现。同时,也提到一些个人设计和实现时候的一些思考,比如多叉树和有向无环图。另外...

LuckyWinty的博客 1711

TI IWR6843AOP雷达+DCA1000EVM数据采集:官方手册里的坑,我帮你踩完了

本文详细解析了TI IWR6843AOP雷达与DCA1000EVM数据采集实战中的12个关键细节,包括硬件连接陷阱、mmWave Studio隐藏设置、数据采集参数优化等,帮助开发者避开官方手册未提及的常见问题,提升数据采集效率和系统稳定性。

weixin_28366475的博客 1102

从入门到精通:MATEKH743 飞控全解析——MAVLink 接口软/硬件对接实战

MATEKH743飞控是基于STM32H743微控制器的无人机核心控制系统,具备强大计算能力和丰富外设接口。该飞控采用双核架构,集成高性能IMU、气压计等传感器,支持多种开源固件(Betaflight/INAV/ArduPilot),适用于FPV穿越机、固定翼及工业无人机。相比PX4等飞控,MATEKH743在性价比和DIY友好度上优势明显,同时支持MAVLink协议实现设备通信。未来发展方向包括集成更高精度传感器、优化电源管理以及拓展工业应用场景,持续推动无人机技术创新。

bother3000的博客 2284

设计一个调度器

异步调度器 Scheduler

IT913913的博客 404

汽车BMS技术分享及其HIL测试方案

在全球碳中和目标的战略驱动下,新能源汽车产业正以指数级速度重塑交通出行格局。动力电池作为电动汽车的"心脏",其性能与安全性不仅直接决定了车辆的续航里程、使用寿命等关键指标,更深刻影响着消费者对电动汽车的市场接受度。而电池管理系统(Battery Management System, BMS)是动力电池安全运行与性能释放的核心技术支撑。

Speedgoat的博客 2684

7种主流案例,告诉你调度器架构设计通用法则(干货!)

女主宣言今天小编为大家转载一篇来自DBAplus社群的干货文章,希望能够帮助大家对关于调度器的理解。作者张晨,Strikingly数据平台工程师,算法、分布式系统和函数式...

HULK一线技术杂谈 3214

Vivado IP核:AXI Uartlite的使用经验分享

1. IP基本设置 在Vivado中,打开IP Catalog,搜索“uart”: 双击“AXI Uartlite”,进入IP配置界面: 这里可以配置IP的具体参数,设置时钟频率、波特率等,完成后,生成IP即可。 2. IP的时序特点 点击下面的链接可以下载该IP核的官方指导文档: https://china.xilinx.com/support/documentation/ip_docume...

weixin_43265132的博客 1万+

这才是 Python 的正确玩法!用强化学习 (RL) 训练 AI 玩《王者荣耀》,胜率 90%!

大家打排位时,一定遇到过那种“意识神级、操作零失误”的对手,或者“走位呆滞、送人头”的队友。以前的“人机”是程序员写死的脚本(If 血量<20% Then 回城)。而现在的顶级 AI(如腾讯绝悟、OpenAI Five),是靠强化学习 (Reinforcement Learning)自己“左右互搏”练出来的。今天,我们不谈枯燥的数学公式,我将手把手教你用,搭建一个能玩 MOBA 游戏的 AI Agent。即使不能去 KPL 打职业,虐一下钻石局还是绰绰有余的!

BUG猿的博客 1182

Go并发任务调度器设计与实现:从理论到实践

在当今高并发、低延迟的后端系统架构中,高效的任务调度能力已成为衡量系统性能的关键指标之一。就像交通枢纽需要智能红绿灯系统一样,复杂的后端服务需要精心设计的并发调度器来协调各类任务的执行节奏,确保系统资源被合理利用。现代后端系统面临的并发场景日益复杂:从简单的API请求处理,到复杂的数据分析任务;从定时任务调度,到实时消息处理。这些任务往往具有不同的优先级、资源需求和执行特性。没有合适的调度机制,系统很容易在高负载下出现资源争抢、处理延迟甚至崩溃的情况。一个优秀的并发调度器能够解决以下核心问题:我们来看几个典

sinat_27016095的博客 899

《用 R 探索医药数据科学》目录(持续更新,已超 250 万字)|订阅前说明

我们重磅推出《用 R 探索医药数据科学》专栏!本文作为专栏【订阅后首读】的核心内容,堪称开启学习之旅的 “入门钥匙”,也是整个专栏最关键的一篇指引文。文中不仅完整呈现了专栏 9 大核心篇章 + 问答板块的详细目录,更囊括了订阅后必知的核心要点 —— 从阅读设备、实操工具的使用建议,到学习路径、更新规则的清晰说明,再到问题反馈、感悟记录的实用方法,一站式帮你摸清专栏框架、找准学习方向。建议各位订阅者务必认真阅读、吃透内容,这能让你后续的学习更高效、少走弯!

MD Code 1万+

深入解析Golang GMP

本文深入解析了 Go 语言中的 GMP 调度模型,结合源码详细讲解了 Goroutine、M(Machine)和 P(Processor)之间的协作机制。首先,介绍了 Goroutine 的生命周期及状态管理,分析了状态切换的关键函数。随后,阐述了调度器通过主动、正常、抢占和被动调度,确保 Goroutine 在多核系统中高效并发执行。文中重点解析了 schedule()、findRunnable() 和 execute() 等核心函数的具体实现,展示了调度器如何通过本地队列、全局队列、工作窃取和网络轮询等

tatasix的博客 1972

音视频OSD——使用freetype将矢量字库的信息叠加至YUV

目录freetypes使用步骤1)包含头文件:2) 初始化库:3)加载face对象:4)设置字体大小5)设置字体位置,以及旋转度数:6)加载字形图像准备矢量字库代码.c.h效果叠加至YUV freetypes FreeType是一个完全开源的、可扩展、可定制且可移植的字体引擎,它提供TrueType字体驱动的实现统一的接口来访问多种字体格式文件 下载freetypes tar -xjf freetype-2.4.10.tar.bz2 cd freetype-2.4.10/ ./configure

路人coder 2364

QMK固件OLED显示屏配置教程:从零开始实现个性化键盘显示(实操部分)

本教程详细介绍了如何从零开始配置QMK固件的OLED显示屏,实现个性化键盘显示。首先,文章解释了OLED显示屏的基本概念及其在键盘中的应用,并介绍了I2C和SPI两种通信方式。接着,教程逐步指导用户如何在QMK固件中启用OLED功能,包括在rules.mk中启用OLED、在halconf.h和mcuconf.h中配置I2C功能,以及在config.h中设置I2C和OLED参数。最后,教程展示了如何在keymap.c中编写代码,实现OLED显示内容,并提供了进阶定制功能,如显示当前层信息、按键状态和QMK L

Despacito0o的博客 580

Goroutine + Channel 高效在哪?一文吃透 Go 并发底层 G-M-P 调度与实现

Go语言的高效并发实现解析 Go语言凭借其独特的并发模型在服务端和云计算领域脱颖而出。其核心优势源于对CSP(通信顺序进程)范式的实践,通过"通过通信来共享内存"的理念取代传统共享内存模型。Go语言通过两个核心组件实现这一哲学:轻量级执行单元Goroutine和通信管道Channel。 Goroutine采用M:N调度模型,将大量应用级并发单元复用到少量OS线程上,极大降低了资源开销。每个Goroutine初始仅分配2KB栈空间,且切换由用户空间的Go运行时管理,避免了昂贵的内核上下文切

1248

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

(Go并发模型全貌解析):从内存模型到调度器的底层原理揭秘

深入掌握go并发编程模型,解析Goroutine、通道与调度器协同机制,涵盖高并发场景下的内存安全与性能优化策略。揭示Go底层原理,提升并发编程效率,值得收藏。

LogicGap的博客 607

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

Go 如何应对高并发 | 高并发详解

并发编程是当前软件领域中一个重要的概念。Go语言通过goroutine和channel的特性,天生支持高并发处理,充分利用现代CPU的多核优势。与Java相比,Go语言的协程更加轻量级,可以轻松创建上百万个协程。Go语言的调度器采用GPM调度模型,通过将协程放入队列中,由调度器分配给CPU处理。此外,Go语言采用CSP模型,通过channel实现协程之间的通信,避免了共享内存带来的竞态问题。相比之下,Go语言的Mutex锁更轻量、灵活,并且具有更高的性能。

Lamb的博客 1893

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

Golang运行时调度器:非均匀访存模型下的调度演进

在现代计算机体系结构中,NUMA(非均匀内存访问)架构已成为主流。作为Go语言核心组件之一的运行时调度器,其设计演进始终与硬件架构的发展保持同步。本文将深入探讨Go调度器在NUMA架构下的现状与未来可能的演进方向。 ## Go调度器的演进历程 ### 1. 单线程调度器时代(Go 1之前) 最初的Go调度器设计极为简单: - 仅支持单线程执行环境 - 最大M(机器线程)数量被硬编码为1 -

gitblog_00807的博客 369

解构Go并发之核,与Dmitry Vyukov共探Go调度艺术

我们将设身处地,从他最初面临的设计目标和挑战开始,一步步看他是如何分析问题、尝试方案、做出权衡,并最终构建出我们今天所熟知的、强大的Go调度器的。无论你是希望在技术深度上更进一步,还是想在面试中展现对Go并发的透彻理解,亦或是对计算机系统底层原理充满好奇,这个微专栏都将是一次不容错过的思想盛宴。优雅的抢占机制又是如何设计的,以保障系统的响应性与GC的顺畅?

TonyBai 586

构建高效 Go 语言并发系统:性能优化之路

M(Machine):代表操作系统线程,每个 M 对应一个操作系统线程,负责执行一个或多个 goroutine。:表示调度器的处理器,它提供了执行 goroutine 所需的上下文环境,包括 goroutine 队列等。P 的数量通常与 CPU 核心数量相关,默认情况下,Go 会根据系统的 CPU 核心数来设置 P 的数量,可通过函数进行调整。每个 P 会绑定到一个 M 上,在该 M 上执行其队列中的 goroutine。

kjj987的博客 1163

Goroutine调度原理精讲

经过对Go调度器的深入剖析,我们已经揭开了goroutine高效运行的神秘面纱。正如武术大师不仅要练习招式,更要理解内功心法,理解调度原理是编写高效Go程序的重要基础。理解goroutine调度原理,就像了解汽车引擎的工作原理一样,让我们能够更加得心应手地驾驭这个强大的工具。在实际开发中,记住:并发不是目的,而是手段;性能优化也不是盲目追求极限,而是在资源、可靠性和开发效率之间找到平衡。希望本文能帮助你更深入地理解Go并发的内部机制,写出更高效、更可靠的Go程序。

sinat_27016095的博客 1970

从操作系统漫谈GOLang GPM模型

内核态(Kernel Mode):内核态是操作系统内核运行的状态。在这个状态下,操作系统可以直接访问硬件资源和系统内存,执行任何 CPU 指令。内核态具有最高的权限,能够执行特权操作,如管理内存、处理硬件中断等。用户态(User Mode):用户态是用户应用程序运行的状态。在用户态下,程序无法直接访问硬件和系统资源,所有对硬件的访问必须通过系统调用(System Call)来实现。用户态的权限较低,目的是保护系统的稳定性和安全性。

本博客聚焦游戏开发技巧、创业实战心得及大学热门课程干货。这里既有技术深度,也有思维广度,无论你是开发者、创业者还是高校学子,都能在这里找到适合自己的成长之路! 1205

Go语言高并发编程全面解析:从基础到高级实战

Go语言采用CSP并发模型,通过Goroutine和Channel实现高效并发。Goroutine是轻量级用户态线程,初始栈仅2-4KB,支持动态扩展,相比传统线程开销更低且更易管理。Channel作为Goroutine间通信机制,通过无缓冲Channel实现强同步,有缓冲Channel支持生产者-消费者模式。Go运行时采用M:N调度模型,高效管理大量Goroutine。标准库提供sync.WaitGroup、Context等工具简化并发控制,使开发者能轻松构建高性能并发应用,避免传统并发编程的复杂性。

才这么点的博客 1141
上一篇: RedisJson特性让我放弃了MongoDB,也让我放弃了Hash,我就问你们香不香?
下一篇: CPU缓存读写以及一致性问题,你大学课堂睡过去的现在再温习下,绝对受益颇多
stackfuture
博客等级 码龄5年 23粉丝 38原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值