【golang】调度系列之m
调度系列
调度系列之goroutine
上一篇中介绍了goroutine,最本质的一句话就是goroutine是用户态的任务。我们通常说的goroutine运行其实严格来说并不准确,因为任务只能被执行。那么goroutine是被谁执行呢?是被m执行。
在GMP的架构中,m代表的是主动执行的能力,一个m对应的是一个线程。注意的是m只是对应操作系统的线程,因为线程是由操作系统来管理的,但是在用户态中我们可以通过一些同步机制来实现一定程度的操纵。
同样类比一个任务系统的话,goroutine对应task,m对应的就是worker。任务系统中创建一定数量的worker,worker获取task并执行,循环往复。通常在简单的任务系统中,只有worker和task两个对象完全可以胜任,所有task出于全局的队列(或者其他数据结构中)。golang的调度系统最开始也确实是GM架构。但是golang的调度体系显然不属于简单的任务系统,所以go在G和M中增加了一个中间层P。P对应的是执行的权限、执行的资源,这个会在下篇介绍。
文章目录
- m的状态图
- m的操作
- newm
- mstart
- mexit
- startm
- stopm
- m的对象
m的状态图
在介绍具体的细节前,同样先来一个整体的状态图。
需要说明的是,m不同于g,g有明确的status字段来记录状态,m没有记录状态的字段。虽然m没有status字段以及可枚举的状态值,但仍然可以抽象出相应的状态,来做状态的流转。
先介绍下几个状态值的含义。
- running。
表示m在运行中。处于running状态的m在执行某个goroutine或者在调用findrunnable寻找可执行的goroutine。需要注意的是,m处于running状态时,其g可能会处于running状态或者syscall状态。 - spinning。
表示m处于自旋状态,m有spinning字段表示是否处于自旋状态。此时系统中没有goroutine可执行时,但是m不会立即挂起,而是尝试寻找可执行的任务。spinning的设计是为了减少线程的切换,因为线程切换的损耗是比较高的。 - idle。
表示m处于空闲状态。此时m位于全局的队列(schedt.midle)中,对应的线程阻塞在condition上,等待唤醒。通常来说,m会在尝试spinning后再切换为idle。但是go中对最大的spinning的数量做了限制,如果正在spining的数量过多,则会直接转换为idle。

m开始创建时会处于running或者spinning状态(哪些情况下会处于spinning状态还不确定)。
当running状态的m找不到可执行的goroutine时,会切换为spinning状态,spinning一段时间后会转变为idle;另一个种情况时,当m从系统调用中返回时,获取不到p,则会转换为spinning状态。
当然我们上面也说过,处于spining状态的m的数量是有限制的,当达到这个限制,running会直接转变为idle。当需要新的m时,会先尝试从schedt.midle这个队列中获取m,如果没有再通过newm进行创建。
m流转的大概情况如此,下面我们来介绍细节。
m的操作
m的操作中,主要涉及到newm、mstart、mexit、startm等几个方法,下面逐一进行介绍。
newm
newm是创建m的入口(应该也是唯一的入口)。newm创建m对象,并将其同os线程关联起来运行,fn为传入的运行的函数。在某些情况下(这里暂时不深究),不能直接创建os线程,通过newmHandoff来操作,代码块中略过。
// src/proc.go 2096
func newm(fn func(), _p_ *p, id int64) {// allocm adds a new M to allm, but they do not start until created by// the OS in newm1 or the template thread.//// doAllThreadsSyscall requires that every M in allm will eventually// start and be signal-able, even with a STW.//// Disable preemption here until we start the thread to ensure that// newm is not preempted between allocm and starting the new thread,// ensuring that anything added to allm is guaranteed to eventually// start.acquirem()mp := allocm(_p_, fn, id)mp.nextp.set(_p_)mp.sigmask = initSigmaskif gp := getg(); gp != nil && gp.m != nil && (gp.m.lockedExt != 0 || gp.m.incgo) && GOOS != "plan9" {...}newm1(mp)releasem(getg().m)
}
newm函数开始时,首先调用acquirem来防止发生抢占,并在结束时调用releasem来解锁。acquirem和releasem是通过对m的locks字段进行操作来达成目的的。
//go:nosplit
func acquirem() *m {_g_ := getg()_g_.m.locks++return _g_.m
}//go:nosplit
func releasem(mp *m) {_g_ := getg()mp.locks--if mp.locks == 0 && _g_.preempt {// restore the preemption request in case we've cleared it in newstack_g_.stackguard0 = stackPreempt}
}
之后调用allocm创建m对象,并做一些初始化的操作,主要是为g0和gsignal分配内存。 g0在上一篇介绍g的时候提到过,这是和每个m绑定的,主要执行系统任务,协程调度等任务都是在g0中执行的。gsignal是为信号处理分配的栈。然后会将m加入全局的队列(allm)中。allocm的代码这里就不贴了,感兴趣可以自己查看。
allocm创建的m调用newm1函数运行。忽略cgo的部分。newm1中调用了newosproc方法来运行m。
func newm1(mp *m) {if iscgo {...}execLock.rlock() // Prevent process clone.newosproc(mp)execLock.runlock()
}
newosproc调用了一些真正的底层方法,在准备工作(略过)之后调用pthread_create创建了os线程。os线程执行的入口为mstart_stub,其会指向mstart,创建的m作为参数传入。通过这里就讲os线程同m关联起来了。
// glue code to call mstart from pthread_create.
func mstart_stub()
// May run with m.p==nil, so write barriers are not allowed.
//
//go:nowritebarrierrec
func newosproc(mp *m) {// 忽略准备工作....// Finally, create the thread. It starts at mstart_stub, which does some low-level// setup and then calls mstart.var oset sigsetsigprocmask(_SIG_SETMASK, &sigset_all, &oset)err = pthread_create(&attr, abi.FuncPCABI0(mstart_stub), unsafe.Pointer(mp))sigprocmask(_SIG_SETMASK, &oset, nil)if err != 0 {write(2, unsafe.Pointer(&failthreadcreate[0]), int32(len(failthreadcreate)))exit(1)}
}
mstart
newm是创建m的入口,mstart是m执行的入口。mstart是汇编实现,调用了mstart0。
// mstart is the entry-point for new Ms.
// It is written in assembly, uses ABI0, is marked TOPFRAME, and calls mstart0.
func mstart()
mstart0初始化了栈相关的字段,是我们在goroutine中提到的stackguard0字段。这里getg()得到的应该是对应m的g0。然后调用mstart1。最后调用mexit。需要注意的是mstart1是不会返回的(这点下面详细介绍),所以不用担心mexit一下就执行了。
func mstart0() {_g_ := getg()osStack := _g_.stack.lo == 0if osStack {...}// Initialize stack guard so that we can start calling regular// Go code._g_.stackguard0 = _g_.stack.lo + _StackGuard// This is the g0, so we can also call go:systemstack// functions, which check stackguard1._g_.stackguard1 = _g_.stackguard0mstart1()// Exit this thread.if mStackIsSystemAllocated() {// Windows, Solaris, illumos, Darwin, AIX and Plan 9 always system-allocate// the stack, but put it in _g_.stack before mstart,// so the logic above hasn't set osStack yet.osStack = true}mexit(osStack)
}
mstart1保证是非内联的,这是为了保证能够记录mstart调用mstart1时的执行状态(pc和sp),将其保存在g0.sched中。这样调用gogo(&g0.sched)能够回到mstart该节点继续执行,后面的就会执行mexit。保证m的退出能够执行mexit。
mstart1中会先调用fn,然后调用schedule。g的介绍中提到过schedule方法是不会返回的,也是前面提到mstart1不会返回的原因。此时,m真正进入不断寻找就绪的g并执行的过程中,也进入了状态图中running、spinning、idle之间不断状态流转的过程中。
// The go:noinline is to guarantee the getcallerpc/getcallersp below are safe,
// so that we can set up g0.sched to return to the call of mstart1 above.
//
//go:noinline
func mstart1() {_g_ := getg()if _g_ != _g_.m.g0 {throw("bad runtime·mstart")}// Set up m.g0.sched as a label returning to just// after the mstart1 call in mstart0 above, for use by goexit0 and mcall.// We're never coming back to mstart1 after we call schedule,// so other calls can reuse the current frame.// And goexit0 does a gogo that needs to return from mstart1// and let mstart0 exit the thread._g_.sched.g = guintptr(unsafe.Pointer(_g_))_g_.sched.pc = getcallerpc()_g_.sched.sp = getcallersp()asminit()minit()// Install signal handlers; after minit so that minit can// prepare the thread to be able to handle the signals.if _g_.m == &m0 {mstartm0()}if fn := _g_.m.mstartfn; fn != nil {fn()}if _g_.m != &m0 {acquirep(_g_.m.nextp.ptr())_g_.m.nextp = 0}schedule()
}
mexit
mexit主要是做一些释放资源的操作,包括:将分配的栈内存释放、从全局的队列中移除m、将持有的p释放移交,然后退出os线程。这里就不做过多的详细的介绍。代码也不贴了,位于 src/go/proc.go 1471
startm
newm是创建m的唯一入口,但实际上大多数时候需要m的时候都是调用了startm。startm和newm的唯一区别时,其会先去全局的空闲队列里寻找,如果找不到再去调用newm进行创建。如果找到了,则获取idle的m,并唤醒该m。
//go:nowritebarrierrec
func startm(_p_ *p, spinning bool) {mp := acquirem()lock(&sched.lock)if _p_ == nil {_p_, _ = pidleget(0)if _p_ == nil {unlock(&sched.lock)if spinning {// The caller incremented nmspinning, but there are no idle Ps,// so it's okay to just undo the increment and give up.if int32(atomic.Xadd(&sched.nmspinning, -1)) < 0 {throw("startm: negative nmspinning")}}releasem(mp)return}}nmp := mget()if nmp == nil {// No M is available, we must drop sched.lock and call newm.// However, we already own a P to assign to the M.//// Once sched.lock is released, another G (e.g., in a syscall),// could find no idle P while checkdead finds a runnable G but// no running M's because this new M hasn't started yet, thus// throwing in an apparent deadlock.//// Avoid this situation by pre-allocating the ID for the new M,// thus marking it as 'running' before we drop sched.lock. This// new M will eventually run the scheduler to execute any// queued G's.id := mReserveID()unlock(&sched.lock)var fn func()if spinning {// The caller incremented nmspinning, so set m.spinning in the new M.fn = mspinning}newm(fn, _p_, id)// Ownership transfer of _p_ committed by start in newm.// Preemption is now safe.releasem(mp)return}unlock(&sched.lock)if nmp.spinning {throw("startm: m is spinning")}if nmp.nextp != 0 {throw("startm: m has p")}if spinning && !runqempty(_p_) {throw("startm: p has runnable gs")}// The caller incremented nmspinning, so set m.spinning in the new M.nmp.spinning = spinningnmp.nextp.set(_p_)notewakeup(&nmp.park)// Ownership transfer of _p_ committed by wakeup. Preemption is now// safe.releasem(mp)
}
stopm
stopm是用来挂起m,其内容也比较简单。将m放置到全局的空闲队列中,然后调用mPark。mPark是一个阻塞的操作,其会阻塞在信号(m.park)上,等待唤醒,然后获取P继续执行。
// Stops execution of the current m until new work is available.
// Returns with acquired P.
func stopm() {_g_ := getg()if _g_.m.locks != 0 {throw("stopm holding locks")}if _g_.m.p != 0 {throw("stopm holding p")}if _g_.m.spinning {throw("stopm spinning")}lock(&sched.lock)mput(_g_.m)unlock(&sched.lock)mPark()acquirep(_g_.m.nextp.ptr())_g_.m.nextp = 0
}
// mPark causes a thread to park itself, returning once woken.
//
//go:nosplit
func mPark() {gp := getg()notesleep(&gp.m.park)noteclear(&gp.m.park)
}
m的对象
m对应结构体的具体的代码就不贴了,这里就挑一些字段进行介绍。有后面涉及到的字段再来补充。

写在最后
本篇呢,依旧是只聚焦于m本身。同样的道理,抛开G和P,很难讲到面面俱到。但是同样的,读完本篇,相信对m也会有一个本质的理解。m就是一个worker,其同一个os线程关联。我们会将活跃的m的数量控制在一定的范围,以避免过多的切换造成不必要的损耗。m在不同条件下会在running、spinning、idle之间进行状态的转换。我们通过不同的队列以及一些同步机制在用户态来管理m。下面可能还会有一篇来补充些M相关的内容,然后再开始P的介绍。
相关文章:
【golang】调度系列之m
调度系列 调度系列之goroutine 上一篇中介绍了goroutine,最本质的一句话就是goroutine是用户态的任务。我们通常说的goroutine运行其实严格来说并不准确,因为任务只能被执行。那么goroutine是被谁执行呢?是被m执行。 在GMP的架构中ÿ…...
可持久化线段树
可持久化线段树 模板 在某一指定版本的单点查,单点修。 开 m m m 棵线段树,每次修改复制后单点修。时间复杂度 O ( m ( n log n ) ) O(m(n\log n)) O(m(nlogn)),空间复杂度 O ( n m ) O(nm) O(nm),不如暴力。 每次修改…...
运行 Node.js 与浏览器 JavaScript
浏览器和 Node.js 都使用 JavaScript 软件语言 - 但字面上的运行时环境是不同的。 Node.js(又名服务器端 JavaScript)与客户端 JavaScript 有许多相似之处。它也有很多差异。 尽管两者都使用 JavaScript 作为软件语言,但我们可以重点关注一些关键差异,这些差异使两者之间…...
File类操作
1. 练习一 在当前模块下的 text 文件夹中创建一个 io.txt 文件 import java.io.File; import java.io.IOException;public class Practice1 {public static void main(String[] args) {File file new File("D:\\kaifamiao");File file1 new File(file, "tex…...
C# 实现电子签名
本项目基于Emgu.CV(C#下OpenCv的封装)开发的,编译器最新版Vs2022,编译环境x86 直接看效果图 1.主页面 2.我们先看手写的方式: 点击确认就到主界面,如下 : 点击自动适配-,再点击生成…...
小米6/6X/米8/米9手机刷入鸿蒙HarmonyOS.4.0系统-刷机包下载-遥遥领先
小米手机除了解锁root权限,刷GSI和第三方ROM也是米粉的一大爱好,这不,在华为发布了HarmonyOS.4.0系统后不久,我们小米用户也成功将自己的手机干山了HarmonyOS.4.0系统。虽然干上去HarmonyOS.4.0系统目前BUG非常多,根本…...
集合框架和泛型二
一、Set接口 1. Set接口概述 java.util.Set 不包含重复元素的集合、不能保证存储的顺序、只允许有一个 null。 public interface Set<E> extends Collection<E>抽象方法,都是继承自 java.util.Collection 接口。 Set 集合的实现类有很多,…...
thinkphp6 入门教程合集(更新中)
thinkphp6 入门(1)--安装、路由规则、多应用模式 thinkphp6 入门(1)--安装、路由规则、多应用模式_软件工程小施同学的博客-CSDN博客 thinkphp6 入门(2)--视图、渲染html页面、赋值 thinkphp6 入门&#…...
openGauss学习笔记-65 openGauss 数据库管理-创建和管理数据库
文章目录 openGauss学习笔记-65 openGauss 数据库管理-创建和管理数据库65.1 前提条件65.2 背景信息65.3 注意事项65.4 操作步骤65.4.1 创建数据库65.4.2 查看数据库65.4.3 修改数据库65.4.4 删除数据库 openGauss学习笔记-65 openGauss 数据库管理-创建和管理数据库 65.1 前提…...
mysql、MHA高可用配置即故障切换
MHA概述 一套优秀的MySQL高可用环境下故障切换和主从复制的软件 MHA的出现就是解决MySQL 单点的问题 MySQL故障过程中,MHA能做到0-30秒内自动完成故障切换 MHA能在故障切换的过程中最大程度上保证数据的一致性以达到真正意义上的高可用 MHA的组成(核…...
使用“vue init mpvue/mpvue-quickstart“初始化mpvue项目时出现的错误及解决办法
当使用"vue init mpvue/mpvue-quickstart"初始化 mpvue 项目时出现 "vue-cli Failed to download repo mpvue/mpvue-quickstart: connect ETIMEDOUT IP地址"原因是 github 的 IP 解析失败,连接超时 解决办法:更改最新的 github 的 …...
Linux-Shell整理集合
Shell变量 参考文章: Shell脚本中变量的使用 shell语法之 , ‘ ‘ , {},, ,‘‘,(),$(())四种语法含义 参考文章: shell语法之 , ‘ ‘ , {},, ,‘‘,(),$(())四种语法含义 grep常用用法 Shell awk命令详解 grep 跟awk连着用: 获取某程序的…...
windows环境下node安装教程(超详细)
安装node.js 1、下载node: 下载地址:下载 | Node.js 中文网 node.js的zip包安装时是直接解压缩后就可以了, node.js的msi包是傻瓜式一路next就可以了 选择一中方式就可以 2、解压后的目录,或者mis安装后的目录如下: 3、安装完后,可以在命令行中输入…...
《TCP/IP网络编程》阅读笔记--并发多进程服务端的使用
目录 1--并发服务器端 2--进程 2-1--进程的相关概念 2-2--fork()创建进程 2-3--僵尸进程 2-4--wait()和waitpid()销毁僵尸进程 3--信号处理 3-1--signal()函数 3-2--sigaction()函数 3--3--利用信号处理技术消灭僵尸进程 4--基于多任务的并发服务器 5--分割 TCP 的…...
【C++】day2学习成果:引用、结构体等等。。。
1.封装一个结构体,结构体中包含一个私有数组,用来存放学生的成绩,包含一个私有变量,用来记录学生个数, 提供一个公有成员函数,void setNum(int num)用于设置学生个数 提供一个公有成员函数:void…...
QT 第五天 TCP通信与数据库
一、数据库增删改查 QT core gui sqlgreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c11# The following define makes your compiler emit warnings if you use # any Qt feature that has been marked deprecated (the exact warnings # depend on your comp…...
Java程序中常用的设计模式有哪些和该种设计模式解决的痛点
设计模式是大量程序员智慧的结晶,是优秀的代码范式,是以前那些大佬程序员的编程经验总结,非常值得学习。 在软件开发中,有许多常用的设计模式,每种模式都解决了特定类型的问题。以下是一些常见的设计模式及其简要介绍&…...
Android12之解析/proc/pid进程参数(一百六十四)
简介: CSDN博客专家,专注Android/Linux系统,分享多mic语音方案、音视频、编解码等技术,与大家一起成长! 优质专栏:Audio工程师进阶系列【原创干货持续更新中……】🚀 人生格言: 人生…...
正儿八经的雅思口语盘丝洞大法学习总结(长期修改更新)针对23.9月考生
目录 开篇语 李仙童口语大法 具体体系内容 说道科技产品或者说非传统物品 part2回答八大准则 【part2回答八大准则】(一) 【part2回答八大准则】(二) 【part3回答七大准则】(一) Part 1 核心体系 …...
算法竞赛入门【码蹄集新手村600题】(MT1260-1280)C语言
算法竞赛入门【码蹄集新手村600题】(MT1260-1280)C语言 目录MT1260 袋鼠躲猫猫MT1261 留下来的才是幸运数MT1262 约数MT1263 最大的三位约数MT1264 完数MT1265 区间完数MT1266 完数与因子MT1267 亏数MT1268 因数的因数MT1269 区间素数MT1270 素数计算MT1271 三生质数…...
网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
conda相比python好处
Conda 作为 Python 的环境和包管理工具,相比原生 Python 生态(如 pip 虚拟环境)有许多独特优势,尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处: 一、一站式环境管理:…...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...
【JavaEE】-- HTTP
1. HTTP是什么? HTTP(全称为"超文本传输协议")是一种应用非常广泛的应用层协议,HTTP是基于TCP协议的一种应用层协议。 应用层协议:是计算机网络协议栈中最高层的协议,它定义了运行在不同主机上…...
渲染学进阶内容——模型
最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...
华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...
【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...
Rust 异步编程
Rust 异步编程 引言 Rust 是一种系统编程语言,以其高性能、安全性以及零成本抽象而著称。在多核处理器成为主流的今天,异步编程成为了一种提高应用性能、优化资源利用的有效手段。本文将深入探讨 Rust 异步编程的核心概念、常用库以及最佳实践。 异步编程基础 什么是异步…...
汇编常见指令
汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX(不访问内存)XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...
tree 树组件大数据卡顿问题优化
问题背景 项目中有用到树组件用来做文件目录,但是由于这个树组件的节点越来越多,导致页面在滚动这个树组件的时候浏览器就很容易卡死。这种问题基本上都是因为dom节点太多,导致的浏览器卡顿,这里很明显就需要用到虚拟列表的技术&…...
