Go并发调度进阶-循环调度,不是你理解的死循环

简介: Go并发调度进阶-循环调度,不是你理解的死循环

640.png

Go并发调度进阶



3. 循环调度


所有的GMP初始化工作都已经完成了,是时候启动运行时调度器了。我们已经知道,当所有准备工作都完成后, 最后一个开始执行的调用就是runtime.mstart了。


mstart主要功能:

  • 确定执行栈的边界
  • 启动mstart1
  • 设置退出线程标记osStack=true
  • 调用mexit(osStack)退出线程


再来看下mstart1:

  • 如果当前m并非m0,那么要求绑定p
  • 开始调用schedule()开始调度


所以我们可以看到调度循环schedule无法返回,因此最后一个mexit目前还不会被执行,因此当下所有的Go程序创建的线程都无法被释放。


1. M与P的绑定


M 与 P 的绑定过程(acquirep函数调用wirep绑定)只是简单的将 P 链表中的 P ,保存到 M 中的 P 指针上。绑定前,P 的状态一定是 _Pidle,绑定后 P 的状态一定为 _Prunning。


// 将 p 绑定到 m,p 和 m 互相引用
 _g_.m.p.set(_p_) // *_g_.m.p = _p_
 _p_.m.set(_g_.m) // *_p_.m = _g_.m
 // 修改 p 的状态
 _p_.status = _Prunning


2. M的park和unpark


无论出于什么原因,当 M 需要被park时,可能会执行stopm调用。此调用会将 M 进行park,并阻塞到它被unpark时。这一过程就是工作线程的暂止(park)复始(unpark)


func stopm() {
 _g_ := getg()
 (...)
 // 将 m 放回到 空闲列表中,因为我们马上就要park了
 lock(&sched.lock)
 mput(_g_.m)
 unlock(&sched.lock)
 // park当前的 M,在此阻塞,直到被唤醒
 notesleep(&_g_.m.park)
 // 清除暂止的 note
 noteclear(&_g_.m.park)
 // 此时已经被unpark,说明有任务要执行
 // 立即 acquire P
 acquirep(_g_.m.nextp.ptr())
 _g_.m.nextp = 0
}


它的流程也非常简单,将 m 放回至空闲列表中,而后使用 note 注册一个暂止通知, 阻塞到它重新被复始。


3. 核心调度schedule


// 调度器的一轮:找到 runnable Goroutine 并进行执行且永不返回
func schedule() {
 _g_ := getg()
 (...)
 top:
   if sched.gcwaiting != 0 {
    // 如果需要 GC,不再进行调度
    gcstopm() //park 还有就是偷取没有的时候也会park
    goto top
   }
   var gp *g
   (...)
   if gp == nil {
   // 说明不在 GC
   // 每调度 61 次,就检查一次全局队列,保证公平性
   // 否则两个 Goroutine 可以通过互相 respawn 一直占领本地的 runqueue
   if _g_.m.p.ptr().schedtick%61 == 0 && sched.runqsize > 0 {
    lock(&sched.lock)
    // 从全局队列中偷 g
    gp = globrunqget(_g_.m.p.ptr(), 1)
    unlock(&sched.lock)
  }
 }
   if gp == nil {
   // 说明不在 gc
   // 两种情况:
   //  1. 普通取
   //  2. 全局队列中偷不到的取
   // 从本地队列中取
   gp, inheritTime = runqget(_g_.m.p.ptr())
   (...)
 }
  if gp == nil {
   // 如果偷都偷不到,则休眠,在此阻塞
   gp, inheritTime = findrunnable()
  }
  // 这个时候一定取到 g 了
  if _g_.m.spinning {
   // 如果 m 是自旋状态,则
   //   1. 从自旋到非自旋
   //   2. 在没有自旋状态的 m 的情况下,再多创建一个新的自旋状态的 m
   resetspinning()
  }
  if gp.lockedm != 0 {
     // 如果 g 需要 lock 到 m 上,则会将当前的 p
     // 给这个要 lock 的 g
     // 然后阻塞等待一个新的 p
     startlockedm(gp)
     goto top
  }
  // 开始执行
  execute(gp, inheritTime)
}


看下execute

func execute(gp *g, inheritTime bool) {
 _g_ := getg()
 // 将 g 正式切换为 _Grunning 状态
 casgstatus(gp, _Grunnable, _Grunning)
 // M 与 G 绑定开始执行
 _g_.m.curg = gp
 gp.m = _g_.m
 // G终于开始执行了
 gogo(&gp.sched)
}


当开始执行 execute 后,g 会被切换到 _Grunning 状态,同时将 m 和 g 进行绑定。最终调用 gogo 开始执行。


gogo的执行是一段汇编代码,晦涩难懂哈。但是大致意思就是完成g0到gp的栈切换,然后开始执行runtime.main函数或者用户自定义的goroutine任务。


执行完成后,main goroutine 直接调用 eixt(0) 退出,普通 goroutine 则调用 goexit -> goexit1 -> mcall,完成普通 goroutine 退出后的清理工作,然后切换到 g0 栈,调用 goexit0 函数,将普通 goroutine 添加到缓存池中,再调用 schedule 函数进行新一轮的调度。


调度太复杂了,大致流程如下:

schedule() -> execute() -> gogo() -> goroutine 任务 -> goexit() -> goexit1() -> mcall() -> goexit0() -> schedule()


可以看出,一轮调度从调用 schedule 函数开始,经过一系列过程再次调用 schedule 函数来进行新一轮的调度,从一轮调度到新一轮调度的过程称之为一个调度循环。


这里说的调度循环是指某一个工作线程的调度循环,而同一个Go 程序中存在多个工作线程,每个工作线程都在进行着自己的调度循环。


4. 小结


由于调度核心太过于复杂,大家只需要了解大致流程或者思路就OK,没必要深入底层的细节。因为陷得越深你就越难理解一些细枝末节,我写到这里放弃了,不想深入研究下去了,只需要知道这个循环调度不是一个死循环就行了,而且调度链中的各个环节也应该大致了解下。好了今天就先到这里,欢迎大家关注转发分享哈。


相关文章
|
5月前
|
人工智能 安全 算法
Go入门实战:并发模式的使用
本文详细探讨了Go语言的并发模式,包括Goroutine、Channel、Mutex和WaitGroup等核心概念。通过具体代码实例与详细解释,介绍了这些模式的原理及应用。同时分析了未来发展趋势与挑战,如更高效的并发控制、更好的并发安全及性能优化。Go语言凭借其优秀的并发性能,在现代编程中备受青睐。
157 33
|
4月前
|
存储 Go 开发者
Go 语言中如何处理并发错误
在 Go 语言中,并发编程中的错误处理尤为复杂。本文介绍了几种常见的并发错误处理方法,包括 panic 的作用范围、使用 channel 收集错误与结果,以及使用 errgroup 包统一管理错误和取消任务,帮助开发者编写更健壮的并发程序。
88 4
Go 语言中如何处理并发错误
|
2月前
|
数据采集 Go API
Go语言实战案例:多协程并发下载网页内容
本文是《Go语言100个实战案例 · 网络与并发篇》第6篇,讲解如何使用 Goroutine 和 Channel 实现多协程并发抓取网页内容,提升网络请求效率。通过实战掌握高并发编程技巧,构建爬虫、内容聚合器等工具,涵盖 WaitGroup、超时控制、错误处理等核心知识点。
|
2月前
|
数据采集 消息中间件 编解码
Go语言实战案例:使用 Goroutine 并发打印
本文通过简单案例讲解 Go 语言核心并发模型 Goroutine,涵盖协程启动、输出控制、主程序退出机制,并结合 sync.WaitGroup 实现并发任务同步,帮助理解 Go 并发设计思想与实际应用。
|
4月前
|
数据采集 安全 Go
Go 语言并发编程基础:Goroutine 的创建与调度
Go 语言的 Goroutine 是轻量级线程,由 runtime 管理,具有启动快、占用小、支持高并发的特点。本章介绍 Goroutine 的基本概念、创建方式(如使用 `go` 关键字或匿名函数)、M:N 调度模型及其工作流程,并探讨其在高并发场景中的应用,帮助理解其高效并发的优势。
|
6月前
|
数据采集 监控 Go
用 Go 实现一个轻量级并发任务调度器(支持限速)
本文介绍了如何用 Go 实现一个轻量级的并发任务调度器,解决日常开发中批量任务处理的需求。调度器支持最大并发数控制、速率限制、失败重试及结果收集等功能。通过示例代码展示了其使用方法,并分析了核心组件设计,包括任务(Task)和调度器(Scheduler)。该工具适用于网络爬虫、批量请求等场景。文章最后总结了 Go 并发模型的优势,并提出了扩展功能的方向,如失败回调、超时控制等,欢迎读者交流改进。
197 25
|
6月前
|
SQL 监控 Go
新一代 Cron-Job分布式调度平台,v1.0.8版本发布,支持Go执行器SDK!
现代化的Cron-Job分布式任务调度平台,支持Go语言执行器SDK,多项核心优势优于其他调度平台。
101 8
|
8月前
|
存储 缓存 安全
Go 语言中的 Sync.Map 详解:并发安全的 Map 实现
`sync.Map` 是 Go 语言中用于并发安全操作的 Map 实现,适用于读多写少的场景。它通过两个底层 Map(`read` 和 `dirty`)实现读写分离,提供高效的读性能。主要方法包括 `Store`、`Load`、`Delete` 等。在大量写入时性能可能下降,需谨慎选择使用场景。
|
10月前
|
并行计算 安全 Go
Go语言中的并发编程:掌握goroutines和channels####
本文深入探讨了Go语言中并发编程的核心概念——goroutine和channel。不同于传统的线程模型,Go通过轻量级的goroutine和通信机制channel,实现了高效的并发处理。我们将从基础概念开始,逐步深入到实际应用案例,揭示如何在Go语言中优雅地实现并发控制和数据同步。 ####
|
11月前
|
存储 Go 开发者
Go语言中的并发编程与通道(Channel)的深度探索
本文旨在深入探讨Go语言中并发编程的核心概念和实践,特别是通道(Channel)的使用。通过分析Goroutines和Channels的基本工作原理,我们将了解如何在Go语言中高效地实现并行任务处理。本文不仅介绍了基础语法和用法,还深入讨论了高级特性如缓冲通道、选择性接收以及超时控制等,旨在为读者提供一个全面的并发编程视角。
224 50