各人好,这里是Good Note,关注 公主号:Goodnote,专栏文章私信限时Free。本文详细介绍Golang的GPM调度器,包括底层源码及其实现,以及一些相干的增补知识。
前情提要
并发与并行
并行 (Parallel)
- 界说:
在同一时候,多条指令在多个处理器上同时实行。
- 宏观: 看起来是一起实行的。
- 微观: 实际上多个使命同时进行。
并发 (Concurrency)
- 界说:
在同一时候只能有一条指令实行,但多个历程指令快速轮换实行,宏观上看像是多个使命同时进行。
- 宏观: 多个历程同时实行。
- 微观: 单一时间段内交替实行。
关键区别
- 并行强调在 同一时候,多个使命在差别的处理器上实行。
- 并发强调在 同一时间段内,多个使命交替实行,给人一种同时进行的假象。
历程和线程的区别
- 资源分配和调度单位
- 历程: 是资源分配的最小单位。
- 线程: 是步伐实行的最小单位(资源调度的最小单位)。
- 地点空间
- 历程: 有独立的地点空间。
- 启动一个历程必要分配地点空间和维护段表,开销较大。
- 线程: 共享历程的地点空间,切换和创建的开销更小。
- 通讯方式
- 线程: 共享地点空间,直接访问全局变量等数据。可以通过共享内存、同步机制(互斥锁(Mutex)、读写锁(RWMutex)、信号量(Semaphore))等进行通讯。
- 历程:
- 同一历程内的线程可以直接共享全局变量,通讯方便。
- 差别历程必要利用 IPC(如管道、共享内存、消息队列、套接字(Socket))进行通讯
- 健壮性
- 历程: 独立运行,一个历程崩溃不会影响其他历程。
- 线程: 多线程中任意一个线程崩溃会导致整个历程终止。
协程
协程是“用户态的轻量级线程”,协程的调度完全由用户控制,不为操作体系所知的,它由编程语言层面实现,上下文切换不必要经过内核态,再加上协程占用的内存空间极小,以是有着非常大的发展潜力。
解决的问题
重要用来解决操作体系线程太“重”的问题,所谓的太重,重要表如今以下两个方面:
- 创建和切换的高开销
- 内存利用浪费
- 体系线程栈空间较大,且一旦创建不可动态缩减或扩展。
协程的上风
- 轻量化
- goroutine 是用户态线程,创建和切换无需进入内核。
- 开销远小于体系线程。
- 机动的栈内存管理
- 启动时栈大小为 2KB。
- 栈可以根据必要自动扩展和收缩。
- 高并发能力
- goroutine 支持创建成千上万甚至上百万的协程并发实行,性能和内存开销较低。
接下来解说Go到并发调度——GPM调度器
Go的并发模型-CSP
常见的并发模型有七种:
- 线程与锁
- 函数式编程
- Clojure之道
- actor
- 通讯顺序历程(CSP)
- 数据级并行
- Lambda架构
Go 语言的并发模型是通讯顺序历程(Communicating Sequential Processes,CSP) 的范型(paradigm),核心观念是将两个并发实行的实体通过通道channel毗连起来,所有的消息都通过channel传输。CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在goroutine之间同步和传递数据的关键数据类型叫作通道(channel)。
channel详情请参考:【TODO】
Go的调度模型-GPM源码
GPM代表了三个角色,分别是Goroutine、Processor、Machine。下面详细介绍他们。
- Goroutine:协程,用go关键字创建的实行体,它对应一个结构体g,结构体里保存了goroutine的堆栈信息。
- Machine:表示操作体系的线程。
- Processor:表示处理器,管理G和M的联系。
Goroutine
Goroutine就是代码中利用go关键词创建的实行单位,也是各人熟知的有“轻量级线程”之称的协程,协程是不为操作体系所知的,它由编程语言层面实现,上下文切换不必要经过内核态,再加上协程占用的内存空间极小,以是有着非常大的发展潜力。
- goroutine创建在操作体系线程底子之上,协程与操作体系线程之间是多对多(M:N)的关系。
- 这里的 M:N 是指M个goroutine运行在N个操作体系线程之上,内核负责调度这N个操作体系线程;N个体系线程又负责调度这M个goroutine。
- 所谓的对goroutine的调度,是指步伐代码按照肯定的算法在适当的时候挑选出合适的goroutine并放到CPU上去运行的过程,这些负责对goroutine进行调度的步伐代码我们称之为goroutine调度器。
在Go语言中,声明一个Goroutine很简单。如下:
为了实现对goroutine的调度,利用 g 结构体来保存CPU寄存器的值以及goroutine的其它一些状态信息。
g 结构体保存了goroutine的所有信息,调度器代码可以通过 g 对象来对goroutine进行调度:
- 当goroutine被调离CPU时,调度器代码负责把CPU寄存器的值保存在g对象的成员变量之中
- 当goroutine被调度起来运行时,调度器代码又负责把g对象的成员变量所保存的寄存器的值恢复到CPU的寄存器。
g 结构体
g结构体用于代表一个goroutine,该结构体保存了goroutine的所有信息,包括栈,gobuf结构体和其它的一些状态信息:
- // 前文所说的g结构体,它代表了一个goroutine
- type g struct {
- // Stack parameters.
- // stack describes the actual stack memory: [stack.lo, stack.hi).
- // stackguard0 is the stack pointer compared in the Go stack growth prologue.
- // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
- // stackguard1 is the stack pointer compared in the C stack growth prologue.
- // It is stack.lo+StackGuard on g0 and gsignal stacks.
- // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
-
- // 记录该goroutine使用的栈
- stack stack // offset known to runtime/cgo
- // 下面两个成员用于栈溢出检查,实现栈的自动伸缩,抢占调度也会用到stackguard0
- stackguard0 uintptr // offset known to liblink
- stackguard1 uintptr // offset known to liblink
- ......
- // 此goroutine正在被哪个工作线程执行
- m *m // current m; offset known to arm liblink
- // 保存调度信息,主要是几个寄存器的值
- sched gobuf
- stktopsp uintptr // 期望 sp 位于栈顶,用于回溯检查
- param unsafe.Pointer // wakeup 唤醒时候传递的参数
-
- ......
- // schedlink字段指向全局运行队列中的下一个g,
- //所有位于全局运行队列中的g形成一个链表
- schedlink guintptr
- ......
- // 抢占调度标志,如果需要抢占调度,设置preempt为true
- preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
- ......
- }
复制代码 g结构体中两个重要的子结构体(stack和gobuf)介绍如下:
stack结构体
stack结构体重要用来记录goroutine所利用的栈的信息,包括栈顶和栈底位置:
- // Stack describes a Go execution stack.
- // The bounds of the stack are exactly [lo, hi),
- // with no implicit data structures on either side.
- //用于记录goroutine使用的栈的起始和结束位置
- type stack struct {
- lo uintptr // 栈顶,指向内存低地址
- hi uintptr // 栈底,指向内存高地址
- }
复制代码 gobuf结构体
gobuf结构体用于保存goroutine的调度信息,重要包括CPU的几个寄存器的值:
- type gobuf struct {
- // The offsets of sp, pc, and g are known to (hard-coded in) libmach.
- //
- // ctxt is unusual with respect to GC: it may be a
- // heap-allocated funcval, so GC needs to track it, but it
- // needs to be set and cleared from assembly, where it's
- // difficult to have write barriers. However, ctxt is really a
- // saved, live register, and we only ever exchange it between
- // the real register and the gobuf. Hence, we treat it as a
- // root during stack scanning, which means assembly that saves
- // and restores it doesn't need write barriers. It's still
- // typed as a pointer so that any other writes from Go get
- // write barriers.
- sp uintptr // 保存CPU的rsp寄存器的值
- pc uintptr // 保存CPU的rip寄存器的值
- g guintptr // 记录当前这个gobuf对象属于哪个goroutine
- ctxt unsafe.Pointer
-
- // 保存系统调用的返回值,因为从系统调用返回之后如果p被其它工作线程抢占,
- // 则这个goroutine会被放入全局运行队列被其它工作线程调度,其它线程需要知道系统调用的返回值。
- ret sys.Uintreg
- lr uintptr
-
- // 保存CPU的rip寄存器的值
- bp uintptr // for GOEXPERIMENT=framepointer
- }
复制代码 要实现对goroutine的调度,仅仅有g结构体对象是不够的,至少还必要一个存放所有(可运行)goroutine的容器,便于工作线程寻找必要被调度起来运行的goroutine。
Go调度器引入了schedt结构体,
- 保存调度器自身的状态信息;
- 保存goroutine的全局运行队列。
schedt结构体
schedt结构体用来保存调度器的状态信息和goroutine的全局运行队列:
schedt结构体被界说成了一个共享的全局变量,如许每个工作线程都可以访问它以及它所拥有的goroutine运行队列,我们称这个运行队列为全局运行队列。
- type schedt struct {
- // accessed atomically. keep at top to ensure alignment on 32-bit systems.
- goidgen uint64
- lastpoll uint64
- lock mutex
- // When increasing nmidle, nmidlelocked, nmsys, or nmfreed, be
- // sure to call checkdead().
- // 由空闲的工作线程组成链表
- midle muintptr // idle m's waiting for work
- // 空闲的工作线程的数量
- nmidle int32 // number of idle m's waiting for work
- nmidlelocked int32 // number of locked m's waiting for work
- mnext int64 // number of m's that have been created and next M ID
- // 最多只能创建maxmcount个工作线程
- maxmcount int32 // maximum number of m's allowed (or die)
- nmsys int32 // number of system m's not counted for deadlock
- nmfreed int64 // cumulative number of freed m's
- ngsys uint32 // number of system goroutines; updated atomically
- // 由空闲的p结构体对象组成的链表
- pidle puintptr // idle p's
- // 空闲的p结构体对象的数量
- npidle uint32
- nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go.
- // Global runnable queue.
- // goroutine全局运行队列
- runq gQueue
- runqsize int32
- ......
- // Global cache of dead G's.
- // gFree是所有已经退出的goroutine对应的g结构体对象组成的链表
- // 用于缓存g结构体对象,避免每次创建goroutine时都重新分配内存
- gFree struct {
- lock mutex
- stack gList // Gs with stacks
- noStack gList // Gs without stacks
- n int32
- }
-
- ......
- }
复制代码 由于全局运行队列是每个工作线程都可以读写的,因此访问它必要加锁,然而在一个繁忙的体系中,加锁会导致严峻的性能问题。
- 调度器为每个工作线程引入了一个私有的局部goroutine运行队列,工作线程优先利用自己的局部运行队列,只有必要时才会去访问全局运行队列,这大大淘汰了锁辩论,进步了工作线程的并发性。
- 在Go调度器源代码中,局部运行队列被包含在p结构体的实例对象之中,参考下面Proccessor中的p结构体。
Processor
Proccessor负责Machine与Goroutine的毗连,它的作用如下:
- 它能提供线程必要的上下文情况,
- 分配G到它应该去的线程上实行。(局部goroutine运行队列被包含在p结构体)
p结构体
同样的,处理器的数量也是默认按照GOMAXPROCS来设置的,与线程的数量一一对应。重要存储
- 性能追踪、垃圾采取、计时器等相干的字段外
- 处理器的待待实行的局部goroutine运行队列。
- type p struct {
- lock mutex
- status uint32 // one of pidle/prunning/...
- link puintptr
- schedtick uint32 // incremented on every scheduler call
- syscalltick uint32 // incremented on every system call
- sysmontick sysmontick // last tick observed by sysmon
- m muintptr // back-link to associated m (nil if idle)
- ......
- // Queue of runnable goroutines. Accessed without lock.
- //本地goroutine运行队列
- runqhead uint32 // 队列头
- runqtail uint32 // 队列尾
- runq [256]guintptr //使用数组实现的循环队列
- // runnext, if non-nil, is a runnable G that was ready'd by
- // the current G and should be run next instead of what's in
- // runq if there's time remaining in the running G's time
- // slice. It will inherit the time left in the current time
- // slice. If a set of goroutines is locked in a
- // communicate-and-wait pattern, this schedules that set as a
- // unit and eliminates the (potentially large) scheduling
- // latency that otherwise arises from adding the ready'd
- // goroutines to the end of the run queue.
- runnext guintptr
- // Available G's (status == Gdead)
- gFree struct {
- gList
- n int32
- }
- ......
- }
复制代码 紧张的全局变量
- allgs []*g // 保存所有的g
- allm *m // 所有的m构成的一个链表,包括下面的m0
- allp []*p // 保存所有的p,len(allp) == gomaxprocs
- ncpu int32 // 系统中cpu核的数量,程序启动时由runtime代码初始化
- gomaxprocs int32 // p的最大值,默认等于ncpu,但可以通过GOMAXPROCS修改
- sched schedt // 调度器结构体对象,记录了调度器的工作状态
- m0 m // 代表进程的主线程
- g0 g // m0的g0,也就是m0.g0 = &g0
复制代码
- 全局变量的初始状态:
- 切片(allgs 和 allp)初始化为空。
- 指针(allm)初始化为 nil。
- 整数变量(如 ncpu 和 gomaxprocs)初始化为 0。
- 结构体(如 sched)的所有成员初始化为对应类型的零值。
- 步伐启动时:
- 这些全局变量逐步被赋值和初始化,表示当出息序的调度器状态、线程信息和可用的处理器。
Machine
M就是对应操作体系的线程
- 最多有 GOMAXPROCS 个活跃线程(M)同时运行,默认情况下 GOMAXPROCS 的值等于 CPU 核心数。
- 每个 M 对应一个 runtime.m 结构体实例。
为什么线程数等于 CPU 核数?
每个线程分配到一个 CPU 核心,可以避免线程的上下文切换,从而淘汰体系开销,进步性能。
m 结构体用来代表工作线程,它保存了g p m 三方的信息:
- m 自身利用的栈信息
- 当前 m 上正在运行的 goroutine
- m 绑定的 p 的信息等
详见下面界说中的表明:
- type m struct {
- // g0主要用来记录工作线程使用的栈信息,在执行调度代码时需要使用这个栈
- // 执行用户goroutine代码时,使用用户goroutine自己的栈,调度时会发生栈的切换
- g0 *g // goroutine with scheduling stack
- // 通过TLS实现m结构体对象与工作线程之间的绑定
- tls [6]uintptr // thread-local storage (for x86 extern register)
- mstartfn func()
- // 指向工作线程正在运行的goroutine的g结构体对象
- curg *g // current running goroutine
-
- // 记录与当前工作线程绑定的p结构体对象
- p puintptr // attached p for executing go code (nil if not executing go code)
- nextp puintptr
- oldp puintptr // the p that was attached before executing a syscall
-
- // spinning状态:表示当前工作线程正在试图从其它工作线程的本地运行队列偷取goroutine
- spinning bool // m is out of work and is actively looking for work
- blocked bool // m is blocked on a note
-
- // 没有goroutine需要运行时,工作线程睡眠在这个park成员上,
- // 其它线程通过这个park唤醒该工作线程
- park note
- // 记录所有工作线程的一个链表
- alllink *m // on allm
- schedlink muintptr
- // Linux平台thread的值就是操作系统线程ID
- thread uintptr // thread handle
- freelink *m // on sched.freem
- ......
- }
复制代码 M里面存了两个比力紧张的东西,一个是g0,一个是curg。
M 和 G 的关系
- 保存 m 利用的调度栈,负责运行时使命的管理,与用户 goroutine 栈分离。
- 调度代码运行时利用 g0 的栈,而用户代码运行时利用用户 goroutine 的栈。
- 重要用于 goroutine 的创建、内存分配、使命切换等运行时调度操作。
- curg
- 当前线程正在运行的用户 goroutine。
- 每个线程在同一时候只能运行一个 goroutine,curg 指向该使命。
M 和 P 的关系
- p
- 当前线程正在运行的处理器。
- 提供实行 Go 代码所需的上下文资源,好比本地运行队列和内存分配缓存。
- nextp
- 暂存处理器,通常用于 M 在必要切换使命时暂存 P。
- oldp
- 体系调用之前的处理器,用于在体系调用结束后恢复原处理器情况。
Go的调度模型-流程
PM绑定
默认启动GOMAXPROCS(四)个线程GOMAXPROCS(四)个处理器,然后相互绑定。
创建G并加入P的私有队列
一旦G被创建,在进行栈信息和寄存器等信息以及调度相干属性更新之后,它就要进到一个P的队列等待发车。
创建多个G,轮流往其他P的私有队列里面放。
P的私有队列满,加入G对应的全局队列
如果有许多G,都塞满了,那就把G塞到全局队列里(候车大厅)。
M通过P斲丧G
除了往里塞之外,M这边还要疯狂往外取:
- 首先去处理器的私有队列里取G实行;
- 如果取完的话就去全局队列取;
- 如果全局队列里也没有的话,就去其他处理器队列里偷。
没有可实行的G,M和P断开绑定
如果哪里都没找到要实行的G,那M就会和P断开关系,然后去睡觉(idle)了。
其他情况
G被阻塞
如果两个Goroutine正在通过channel执利用命时阻塞,PM会与G立即断开,找新的G继续实行。
P随M进入体系调用
如果G进行了体系调用syscall,M也会跟着进入体系调用状态,那么这个P留在这里就浪费了,P不会等待G和M体系调用完成,而是P与GM立即断开,找其他比力闲的M实行其他的G。
当G完成了体系调用,由于要继续往下实行,以是必须要再找一个空闲的处理器发车。
如果没有空闲的处理器了,那就只能把G放回全局队列当中等待分配。
监控线程sysmon
sysmon 是 Go 运行时中的一个特殊线程(M),也被称为 监控线程。就像 保洁阿姨,定时清理体系中“不干活”的资源,确保调度器的正常运行和体系的高效运转。
- 特点:
- 不必要 P(处理器)即可独立运行。
- 每 20 微秒到 10 毫秒 被唤醒一次,频率动态调解,实行体系级的维护使命。
- 重要职责:
- 垃圾采取
- 实行垃圾采取相干的辅助使命,释放不再利用的内存资源。
- 采取长时间阻塞的 P
- 当某个 P 长时间处于体系调用或其他阻塞状态时,sysmon 会将其从 M 中解绑,使资源得到重新利用。
- 发起抢占调度
- 监控长时间运行的 G(goroutine),如果某个 G 运行时间过长,sysmon 会发出抢占信号,强制切换到其他 G。
- 确保调度的公平性,防止某个 goroutine 独占资源。
想在步伐中实现抢占,可以利用: runtime.Gosched()
runtime 包及相干功能
runtime.Gosched()
- 作用:
- 将当前的 goroutine 停息,让出 CPU 时间片,让调度器切换其他的 goroutine 实行。
- 当前的 goroutine 会在稍后重新进入运行状态。
- 利用场景:
- 实现协作式的使命切换,避免单个 goroutine 长时间占用 CPU。
runtime.Goexit()
- 作用:
- 终止当前 goroutine,并实行该 goroutine 的 defer 语句。
- 留意事项:
- 不会影响其他 goroutine 或整个步伐的运行,仅结束调用它的 goroutine。
- 用于提前退出某些使命。
runtime.GOMAXPROCS(n int)
- 作用:
- 设置 Go 运行时利用的最大 CPU 核心数。
- 返回之前设置的值。
- 默认值:
- Go 1.5 之前,默认利用单核。
- Go 1.5 及之后,默认利用所有 CPU 核心数。
- 作用表明:
- 决定同时运行的体系线程(M)的数量,对调度性能有直接影响。
- 利用场景:
runtime.NumGoroutine()
- 作用:
- 利用场景:
- 用于监控 goroutine 的数量,帮助判定步伐是否有潜在的内存或调度问题。
GMP 限定分析
M 的限定
- M 是什么:
- 限定:
- 默认最大数量为 10000。
- 超过限定会报错:GO: runtime: program exceeds 10000-thread limit。
- 异常场景:
- 通常只有当大量 goroutine 阻塞时,才会触发这种限定。
- 调解方法:
- 利用 debug.SetMaxThreads 增长限定。
G 的限定
- G 是什么:
- G 代表 goroutine,是 Go 的轻量级线程。
- 限定:
- 没有硬性限定,理论上可以创建任意数量的 goroutine。
- 实际数量受 内存大小 的限定。
- 内存占用:
- 每个 goroutine 约莫占用 2~4 KB 的连续内存块。
- 留意事项:
- 申请的 goroutine过多,如果内存不足,大概会导致创建失败或体系崩溃。当然 这种情况出如今 Goroutine 数量非常庞大(如数百万)。几十个或几百个 Goroutine: 一般是合理的,详细需根据使命性子决定。
P 的限定
- P 是什么:
- 限定:
- 数量由 GOMAXPROCS 参数决定。
- 默认值等于 CPU 核心数。
- 影响:
- P 的数量影响使命并行实行的水平,但不影响 goroutine 的创建数量。
- 合理设置:
历史文章
MySQL数据库
MySQL数据库
Redis
Redis数据库笔记合集
Golang
- Golang笔记——语言底子知识
- Golang笔记——切片与数组
- Golang笔记——hashmap
- Golang笔记——rune和byte
- Golang笔记——channel
- Golang笔记——Interface类型
- Golang笔记——数组、Slice、Map、Channel的并发安全性
- Golang笔记——协程同步
- Golang笔记——并发控制
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |