Golang笔记——GPM调度器

打印 上一主题 下一主题

主题 1031|帖子 1031|积分 3093

各人好,这里是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很简单。如下:
  1. go func() {}()
复制代码
为了实现对goroutine的调度,利用 g 结构体来保存CPU寄存器的值以及goroutine的其它一些状态信息。
g 结构体保存了goroutine的所有信息,调度器代码可以通过 g 对象来对goroutine进行调度:


  • 当goroutine被调离CPU时,调度器代码负责把CPU寄存器的值保存在g对象的成员变量之中
  • 当goroutine被调度起来运行时,调度器代码又负责把g对象的成员变量所保存的寄存器的值恢复到CPU的寄存器。
g 结构体

g结构体用于代表一个goroutine,该结构体保存了goroutine的所有信息,包括栈,gobuf结构体和其它的一些状态信息:
  1. // 前文所说的g结构体,它代表了一个goroutine
  2. type g struct {
  3.     // Stack parameters.
  4.     // stack describes the actual stack memory: [stack.lo, stack.hi).
  5.     // stackguard0 is the stack pointer compared in the Go stack growth prologue.
  6.     // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
  7.     // stackguard1 is the stack pointer compared in the C stack growth prologue.
  8.     // It is stack.lo+StackGuard on g0 and gsignal stacks.
  9.     // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
  10.     // 记录该goroutine使用的栈
  11.     stack       stack   // offset known to runtime/cgo
  12.     // 下面两个成员用于栈溢出检查,实现栈的自动伸缩,抢占调度也会用到stackguard0
  13.     stackguard0 uintptr // offset known to liblink
  14.     stackguard1 uintptr // offset known to liblink
  15.     ......
  16.     // 此goroutine正在被哪个工作线程执行
  17.     m              *m      // current m; offset known to arm liblink
  18.     // 保存调度信息,主要是几个寄存器的值
  19.     sched          gobuf
  20.     stktopsp      uintptr  // 期望 sp 位于栈顶,用于回溯检查
  21.     param         unsafe.Pointer // wakeup 唤醒时候传递的参数
  22.     ......
  23.     // schedlink字段指向全局运行队列中的下一个g,
  24.     //所有位于全局运行队列中的g形成一个链表
  25.     schedlink      guintptr
  26.     ......
  27.     // 抢占调度标志,如果需要抢占调度,设置preempt为true
  28.     preempt        bool       // preemption signal, duplicates stackguard0 = stackpreempt
  29.    ......
  30. }
复制代码
g结构体中两个重要的子结构体(stack和gobuf)介绍如下:
stack结构体

stack结构体重要用来记录goroutine所利用的栈的信息,包括栈顶和栈底位置:
  1. // Stack describes a Go execution stack.
  2. // The bounds of the stack are exactly [lo, hi),
  3. // with no implicit data structures on either side.
  4. //用于记录goroutine使用的栈的起始和结束位置
  5. type stack struct {  
  6.     lo uintptr   // 栈顶,指向内存低地址
  7.     hi uintptr   // 栈底,指向内存高地址
  8. }
复制代码
gobuf结构体

gobuf结构体用于保存goroutine的调度信息,重要包括CPU的几个寄存器的值:
  1. type gobuf struct {
  2.     // The offsets of sp, pc, and g are known to (hard-coded in) libmach.
  3.     //
  4.     // ctxt is unusual with respect to GC: it may be a
  5.     // heap-allocated funcval, so GC needs to track it, but it
  6.     // needs to be set and cleared from assembly, where it's
  7.     // difficult to have write barriers. However, ctxt is really a
  8.     // saved, live register, and we only ever exchange it between
  9.     // the real register and the gobuf. Hence, we treat it as a
  10.     // root during stack scanning, which means assembly that saves
  11.     // and restores it doesn't need write barriers. It's still
  12.     // typed as a pointer so that any other writes from Go get
  13.     // write barriers.
  14.     sp   uintptr  // 保存CPU的rsp寄存器的值
  15.     pc   uintptr  // 保存CPU的rip寄存器的值
  16.     g    guintptr // 记录当前这个gobuf对象属于哪个goroutine
  17.     ctxt unsafe.Pointer
  18.     // 保存系统调用的返回值,因为从系统调用返回之后如果p被其它工作线程抢占,
  19.     // 则这个goroutine会被放入全局运行队列被其它工作线程调度,其它线程需要知道系统调用的返回值。
  20.     ret  sys.Uintreg  
  21.     lr   uintptr
  22.     // 保存CPU的rip寄存器的值
  23.     bp   uintptr  // for GOEXPERIMENT=framepointer
  24. }
复制代码
要实现对goroutine的调度,仅仅有g结构体对象是不够的,至少还必要一个存放所有(可运行)goroutine的容器,便于工作线程寻找必要被调度起来运行的goroutine。
Go调度器引入了schedt结构体,


  • 保存调度器自身的状态信息;
  • 保存goroutine的全局运行队列。

schedt结构体

schedt结构体用来保存调度器的状态信息和goroutine的全局运行队列
schedt结构体被界说成了一个共享的全局变量,如许每个工作线程都可以访问它以及它所拥有的goroutine运行队列,我们称这个运行队列为全局运行队列。
  1. type schedt struct {
  2.     // accessed atomically. keep at top to ensure alignment on 32-bit systems.
  3.     goidgen  uint64
  4.     lastpoll uint64
  5.     lock mutex
  6.     // When increasing nmidle, nmidlelocked, nmsys, or nmfreed, be
  7.     // sure to call checkdead().
  8.     // 由空闲的工作线程组成链表
  9.     midle        muintptr // idle m's waiting for work
  10.     // 空闲的工作线程的数量
  11.     nmidle       int32    // number of idle m's waiting for work
  12.     nmidlelocked int32    // number of locked m's waiting for work
  13.     mnext        int64    // number of m's that have been created and next M ID
  14.     // 最多只能创建maxmcount个工作线程
  15.     maxmcount    int32    // maximum number of m's allowed (or die)
  16.     nmsys        int32    // number of system m's not counted for deadlock
  17.     nmfreed      int64    // cumulative number of freed m's
  18.     ngsys uint32 // number of system goroutines; updated atomically
  19.     // 由空闲的p结构体对象组成的链表
  20.     pidle      puintptr // idle p's
  21.     // 空闲的p结构体对象的数量
  22.     npidle     uint32
  23.     nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go.
  24.     // Global runnable queue.
  25.     // goroutine全局运行队列
  26.     runq     gQueue
  27.     runqsize int32
  28.     ......
  29.     // Global cache of dead G's.
  30.     // gFree是所有已经退出的goroutine对应的g结构体对象组成的链表
  31.     // 用于缓存g结构体对象,避免每次创建goroutine时都重新分配内存
  32.     gFree struct {
  33.         lock          mutex
  34.         stack        gList // Gs with stacks
  35.         noStack   gList // Gs without stacks
  36.         n              int32
  37.     }
  38.     ......
  39. }
复制代码
由于全局运行队列是每个工作线程都可以读写的,因此访问它必要加锁,然而在一个繁忙的体系中,加锁会导致严峻的性能问题。


  • 调度器为每个工作线程引入了一个私有的局部goroutine运行队列,工作线程优先利用自己的局部运行队列,只有必要时才会去访问全局运行队列,这大大淘汰了锁辩论,进步了工作线程的并发性。
  • 在Go调度器源代码中,局部运行队列被包含在p结构体的实例对象之中,参考下面Proccessor中的p结构体。
Processor

Proccessor负责Machine与Goroutine的毗连,它的作用如下:


  • 它能提供线程必要的上下文情况,
  • 分配G到它应该去的线程上实行。(局部goroutine运行队列被包含在p结构体)
p结构体

同样的,处理器的数量也是默认按照GOMAXPROCS来设置的,与线程的数量一一对应。重要存储


  • 性能追踪、垃圾采取、计时器等相干的字段外
  • 处理器的待待实行的局部goroutine运行队列。
  1. type p struct {
  2.     lock mutex
  3.     status       uint32 // one of pidle/prunning/...
  4.     link            puintptr
  5.     schedtick   uint32     // incremented on every scheduler call
  6.     syscalltick  uint32     // incremented on every system call
  7.     sysmontick  sysmontick // last tick observed by sysmon
  8.     m                muintptr   // back-link to associated m (nil if idle)
  9.     ......
  10.     // Queue of runnable goroutines. Accessed without lock.
  11.     //本地goroutine运行队列
  12.     runqhead uint32  // 队列头
  13.     runqtail uint32     // 队列尾
  14.     runq     [256]guintptr  //使用数组实现的循环队列
  15.     // runnext, if non-nil, is a runnable G that was ready'd by
  16.     // the current G and should be run next instead of what's in
  17.     // runq if there's time remaining in the running G's time
  18.     // slice. It will inherit the time left in the current time
  19.     // slice. If a set of goroutines is locked in a
  20.     // communicate-and-wait pattern, this schedules that set as a
  21.     // unit and eliminates the (potentially large) scheduling
  22.     // latency that otherwise arises from adding the ready'd
  23.     // goroutines to the end of the run queue.
  24.     runnext guintptr
  25.     // Available G's (status == Gdead)
  26.     gFree struct {
  27.         gList
  28.         n int32
  29.     }
  30.     ......
  31. }
复制代码
紧张的全局变量

  1. allgs     []*g     // 保存所有的g
  2. allm       *m    // 所有的m构成的一个链表,包括下面的m0
  3. allp       []*p    // 保存所有的p,len(allp) == gomaxprocs
  4. ncpu             int32   // 系统中cpu核的数量,程序启动时由runtime代码初始化
  5. gomaxprocs int32   // p的最大值,默认等于ncpu,但可以通过GOMAXPROCS修改
  6. sched      schedt     // 调度器结构体对象,记录了调度器的工作状态
  7. m0  m       // 代表进程的主线程
  8. 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 的信息等
详见下面界说中的表明:
  1. type m struct {
  2.     // g0主要用来记录工作线程使用的栈信息,在执行调度代码时需要使用这个栈
  3.     // 执行用户goroutine代码时,使用用户goroutine自己的栈,调度时会发生栈的切换
  4.     g0      *g     // goroutine with scheduling stack
  5.     // 通过TLS实现m结构体对象与工作线程之间的绑定
  6.     tls           [6]uintptr   // thread-local storage (for x86 extern register)
  7.     mstartfn      func()
  8.     // 指向工作线程正在运行的goroutine的g结构体对象
  9.     curg          *g       // current running goroutine
  10.     // 记录与当前工作线程绑定的p结构体对象
  11.     p             puintptr // attached p for executing go code (nil if not executing go code)
  12.     nextp         puintptr
  13.     oldp          puintptr // the p that was attached before executing a syscall
  14.    
  15.     // spinning状态:表示当前工作线程正在试图从其它工作线程的本地运行队列偷取goroutine
  16.     spinning      bool // m is out of work and is actively looking for work
  17.     blocked       bool // m is blocked on a note
  18.    
  19.     // 没有goroutine需要运行时,工作线程睡眠在这个park成员上,
  20.     // 其它线程通过这个park唤醒该工作线程
  21.     park          note
  22.     // 记录所有工作线程的一个链表
  23.     alllink       *m // on allm
  24.     schedlink     muintptr
  25.     // Linux平台thread的值就是操作系统线程ID
  26.     thread        uintptr // thread handle
  27.     freelink      *m      // on sched.freem
  28.     ......
  29. }
复制代码
M里面存了两个比力紧张的东西,一个是g0,一个是curg。
M 和 G 的关系


  • g0


  • 保存 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)的数量,对调度性能有直接影响。

  • 利用场景:

    • 限定步伐对 CPU 核心的利用,避免过分竞争。


runtime.NumGoroutine()



  • 作用:

    • 返回当出息序启动的 goroutine 数量。

  • 利用场景:

    • 用于监控 goroutine 的数量,帮助判定步伐是否有潜在的内存或调度问题。


GMP 限定分析

M 的限定



  • 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 是什么:

    • P 代表调度器中的处理器,负责毗连 G 和 M。

  • 限定:

    • 数量由 GOMAXPROCS 参数决定。
    • 默认值等于 CPU 核心数。

  • 影响:

    • P 的数量影响使命并行实行的水平,但不影响 goroutine 的创建数量。

  • 合理设置:

    • 一般情况下,设置为呆板的 CPU 核数即可。


历史文章

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企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

数据人与超自然意识

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表