【golang】调度系列之m

news2025/1/20 12:41:57

调度系列
调度系列之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

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 = initSigmask
   if 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 sigset
   sigprocmask(_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 == 0
   if 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_.stackguard0
   mstart1()

   // 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 = spinning
   nmp.nextp.set(_p_)
   notewakeup(&nmp.park)
   // Ownership transfer of _p_ committed by wakeup. Preemption is now
   // safe.
   releasem(mp)
}

写在最后
本篇呢,依旧是只聚焦于m本身。同样的道理,抛开G和P,很难讲到面面俱到。但是同样的,读完本篇,详细对m也会有一个本质的理解。m就是一个worker,其同一个os线程关联。我们通过一些同步机制在用户态控制m的运行。

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

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

相关文章

PC首页资源加载速度由8s降到2s的优化实践

随着需求的不断开发&#xff0c;前端项目不断膨胀&#xff0c;业务提出&#xff1a;你们的首页加载也太慢啦&#xff0c;我都需要7、8秒才能看到内容&#xff0c;于是乎主管就让我联合后端开启优化专项&#xff0c;目标是3s内展示完全首页的内容。 性能指标 开启优化时&#…

G0第28章:Go语言微服务框架

Go-kit Go kit教程04——中间件和日志 本文主要介绍了Go kit 中的中间件&#xff0c;并以日志中间件为例演示了如何设计和实现中间件。 上一篇中&#xff0c;我们对go kit搭建的项目进行了目录结构拆分 中间件 在 go kit 中&#xff0c;它对中间件的定义是一个接收Endpoint…

DataGridView绑定数据更新

1、创建数据类 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace DataGridViewTest {internal class UserData{public string Name { get; set; }public int Weight { get; set; }public int …

“内存炸弹”DDOS拒绝服务攻击

Windows平台演示 最早的内存炸弹是 zip 炸弹&#xff0c;也称为死亡 zip&#xff0c;它是一种恶意计算机文件&#xff0c;旨在使读取该文件的程序崩溃或瘫痪。zip 炸弹不会劫持程序的操作&#xff0c;而是利用解压缩压缩文件所需的时间、磁盘空间或内存。 zip 炸弹的一个示例…

ConnectionError: Error connecting to Visdom server

pip install visdom python -m visdom.server点击网站即可访问

Modelsim仿真问题解疑三:LM_LICENSE_FILE与Vivado命名冲突

现象: modelsim和Vivado同一时间只能使用一个&#xff0c;另一个会报license相关的错误 原因&#xff1a; modelsim和Vivado的环境变量名称都为LM_LICENSE_FILE&#xff0c;值配置为其中一个时会导致另一个值被覆盖 解决&#xff1a; 对LM_LICENSE_FILE同时配置modelsim和v…

win10环境安装使用docker-maxwell

目的&#xff1a;maxwell可以监控mysql数据变化&#xff0c;并同步到kafka、mq或tcp等。 maxwell和canal区别&#xff1a; maxwell更轻量&#xff0c;canal把表结构也输出了 docker bootstrap可导出历史数据&#xff0c;canal不能 环境 &#xff1a;win10&#xff0c;mysql5…

反编译小程序 SyntaxError: Unexpected token ‘}‘ 异常处理

反编译小程序出现异常&#xff1a; SyntaxError: Unexpected token ‘}’ 网上很多都说使用最新版本的反编译 wxappUnpacker-master 包可以进行解析&#xff0c;但是大神已经停止了更新wxappUnpacker-master 包&#xff1b; 查找了网上大部分的wxappUnpacker-master 包&#…

查看mysql数据库的charset和collation

SELECT * FROM information_schema.SCHEMATA WHERE schema_name test_data; 发现&#xff1a; chaset是utf8mb4&#xff0c;collation是utf8mb4_generic_ci 可笑的是我导入sql脚本&#xff0c;要把脚本中所有的utf8mb4改为utf8&#xff0c;将utf8mb4_generic_ci为utf8_unico…

有哪些适合初学者的编程语言?

C语言 那为什么我还要教你C语言呢&#xff1f;因为我想要让你成为一个更好、更强大的程序员。如果你要变得更好&#xff0c;C语言是一个极佳的选择&#xff0c;其原因有二。首先&#xff0c;C语言缺乏任何现代的安全功能&#xff0c;这意味着你必须更为警惕&#xff0c;时刻了…

日志平台搭建第二章:Linux使用docker安装elasticsearch-head

一、elasticsearch-head的安装启动 #下载镜像 docker pull alivv/elasticsearch-head #启动 docker run -d --name eshead -p 9100:9100 alivv/elasticsearch-head 查看日志 docker logs -f eshead 出现如下证明启动成功 浏览器访问9100端口&#xff0c;出现以下页面也说明…

End-to-end 3D Human Pose Estimation with Transformer

基于Transformer的端到端三维人体姿态估计 摘要 基于Transformer的架构已经成为自然语言处理中的常见选择&#xff0c;并且现在正在计算机视觉任务中实现SOTA性能&#xff0c;例如图像分类&#xff0c;对象检测。然而&#xff0c;卷积方法在3D人体姿态估计的许多方法中仍然保…

vue学习之基本用法

1. 前期准备 安装vs code IDE&#xff0c;vs code 安装 插件 open in brower新建 vue-learning 文件夹vs code IDE打开文件夹 2. 基本用法 创建demo1.html文件,内容如下 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8&qu…

Converting Phase Noise to Random Jitter(Cycle-to-Cycle)

借用Phase Noise to Random Jitter(Period)的转换过程推导了Cycle to Cycle random Jitter&#xff0c;一般展频时钟调制,用来评估相邻周期的随机抖动。

AMD锐龙R5600GVEGA7 GPU环境搭建

AMD的GPU驱动很早就合入LINUX开源主线了,非常适合对GPU分析和学习,所以组装了一台搭配AMD锐龙R5 5600G CPU主机,配有VEGA7核显。不过,经过测试,由于是2021年的产品,所以需要安装较新的LINUX发行版,至少是UBUNTU20.04之后的,主机环境简单记录如下: 配置参数: 基础评分…

C语言之指针进阶篇(2)

目录 函数指针 函数名和&函数名 函数指针的定义 函数指针的使用 函数指针陷阱 代码1 代码2 注意 函数指针数组定义 函数指针数组的使用 指向函数指针数组的指针 书写 终于军训圆满结束了&#xff0c;首先回顾一下指针进阶篇&#xff08;1&#xff09;主要是…

C语言实现三字棋

实现以下&#xff1a; 1游戏不退出&#xff0c;继续玩下一把&#xff08;循环&#xff09; 2应用多文件的形式完成 test.c. --测试游戏 game.c -游戏函数的实现 game.h -游戏函数的声明 (2)游戏再走的过程中要进行数据的存储&#xff0c;可以使用3*3的二维数组 char bor…

【送书活动】揭秘分布式文件系统大规模元数据管理机制——以Alluxio文件系统为例

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…

Modelsim仿真问题解疑二:ERROR: [USF-ModelSim-70]

现象&#xff1a;在Vivado中已配置modelsim为仿真工具后&#xff0c;运行仿真&#xff0c;报错USF-ModelSim-70和ERROR: [Vivado 12-4473] 详细报错内容如下 ERROR: [USF-ModelSim-70] compile step failed with error(s) while executing C:/Users/ZYP_PC/Desktop/verilog_t…

多线程与高并发——并发编程(7)

文章目录 七、JUC并发工具1 CountDownLatch应用&源码分析1.1 CountDownLatch介绍1.2 CountDownLatch应用1.3 CountDownLatch源码分析1.3.1 有参构造1.3.2 await 方法1.3.3 countDown方法2 CyclicBarrier应用&源码分析2.1 CyclicBarrier介绍2.2 CyclicBarrier应用2.3 Cy…