golang的GMP调度浅析

in #cnlast month

GMP数据结构

image.png

  • G: 表示goroutine,包含了协程的状态,栈,上下文等信息。(基础大小2kb,可理解为打包代码段)

  • M: 表示machine, 也就是工作线程,就是真正用来执行代码的线程。
    包含线程的状态,寄存器,信号数据等,由go routime管理生命周期。(2个特殊的M,一个是主线程M,专门用来处理主线流程;另外一个是监控线程M,无需P可执行。
    每个M有三个G。gsingal专门用来处理runtime信号唤醒,另一个g0,是用于执行runtime调度systemstack空间;最后一个是用户代码g。

  • P: 逻辑处理器,包含运行和就绪的G队列,以及本地缓存和定时器相关数据信息,是M工作上下文需要的环境。
    当P有G任务,就需要唤醒或者创建一个M来执行P队列中的任务。(比较用来体现多核并发处理能力,runtime.GOMAXPROCS()可以指定这个P数量,在runtime中,就是一个[]p)
    通常 P:M = 1:1, P属性比较重要字段:runnext, 表示下一个要执行的G;还有一个256大小的本地队列runq

G: 主要保存goroutine的一些状态信息和CPU一些寄存器值,例如将被中断或者恢复调度过程中保存读取寄存器值。

type gobuf struct {
    sp   uintptr  // 堆栈指针,表示g当前的堆栈位置
    pc   uintptr   // 程序计数器,表示当前g执行的指令地址
    g    guintptr  // 当前g的指针引用
    ctxt unsafe.Pointer  // 用于切换和恢复当前关联g时的上下文数据
    ret  sys.Uintreg   // 保存函数调用返回值
    lr   uintptr
    bp   uintptr //  基指针,表示当前函数的栈帧
}

// sudog(也称为唤醒原语,synchronization descriptor)是一种在 Golang 的运行时系统中用于实现 Goroutine 的等待和唤醒机制的数据结构
type sudog struct {
    // The following fields are protected by the hchan.lock of the
    // channel this sudog is blocking on. shrinkstack depends on
    // this for sudogs involved in channel ops.

    g *g

    next *sudog
    prev *sudog
    elem unsafe.Pointer // data element (may point to stack)

    // The following fields are never accessed concurrently.
    // For channels, waitlink is only accessed by g.
    // For semaphores, all fields (including the ones above)
    // are only accessed when holding a semaRoot lock.

    acquiretime int64
    releasetime int64
    ticket      uint32

    // isSelect indicates g is participating in a select, so
    // g.selectDone must be CAS'd to win the wake-up race.
    isSelect bool

    // success indicates whether communication over channel c
    // succeeded. It is true if the goroutine was awoken because a
    // value was delivered over channel c, and false if awoken
    // because c was closed.
    success bool

    parent   *sudog // semaRoot binary tree
    waitlink *sudog // g.waiting list or semaRoot
    waittail *sudog // semaRoot
    c        *hchan // channel
}



type g struct {
    stack       stack   // 堆栈参数,描述实际的堆栈内存,包括栈底和栈顶指针
    stackguard0 uintptr // 用于栈的扩张和收缩检查,将战标志,检测栈溢出情况
    stackguard1 uintptr // offset known to liblink

    _panic       *_panic // 指向当前g的panic信息,用于异常处理
    _defer       *_defer // 指向当前g的延迟调用(defer信息),用于处理延迟函数的执行
    m            *m      // 指向执行该g的工作线程m执行,用于g的调度和执行
    sched        gobuf   // g的运行状态和上下文信息
    atomicstatus uint32  // 用于表示Goroutine的状态,比如运行状态、等待状态、休眠状态等
    goid         int64

    // 表示 g 是否收到了抢占信号, preempt 字段为 true 时,表示该 Goroutine 已经收到了抢占信号
    // 抢占信号的目的是中断当前 Goroutine 的执行,以便调度器可以切换到其他 Goroutine
    preempt       bool 


    // 表示 Goroutine(g结构体)的堆栈是否已经被垃圾回收器扫描完成.
    // 在进行垃圾回收时,垃圾回收器需要遍历并扫描 Goroutine 的堆栈,以确定堆栈上的对象是否可以被回收。
    // 只有在进行堆栈扫描的 Goroutine(_Gscan状态的保护) 才能更新 gcscandone 字段。
    gcscandone   bool // g has scanned stack; protected by _Gscan bit in status


    // 用于存储与当前 Goroutine 相关联的锁定的 m(调度器线程)。
    //  Goroutine 成功获得一个锁时,lockedm 字段会被设置为持有该锁的 m 的指针。
    // 通过跟踪 lockedm 字段,运行时系统可以了解当前 Goroutine 所持有的锁和关联的调度器线程。
    lockedm        muintptr

    // 用于存储与 Goroutine 相关的信号信息。
    // 信号是在操作系统级别产生的事件或通知,可能包含中断、错误、终止等。
    sig            uint32

    // sigcode0 字段用于表示 Goroutine 接收到的信号的代码。信号代码是与特定信号相关联的标识符,用于描述信号的类型和原因。
    sigcode0       uintptr  

    // sigcode0 用于存储接收到的信号的主要代码,而 sigcode1 则用于存储与信号相关的附加信息或辅助代码
    sigcode1       uintptr 
    sigpc          uintptr

    // gopc用于存储该goroutine的go语句的程序计数器(program counter)值。
    gopc           uintptr         // pc of go statement that created this goroutine

    // 用于存储Goroutine 函数的起始指令的地址.每个 Goroutine 都与一个特定的函数关联,该函数定义了 Goroutine 的入口点和要执行的代码。
    startpc        uintptr         // pc of goroutine function

    // waiting 字段是一个指向 sudog 结构体的指针,用于存储当前 Goroutine 等待的 sudog 结构体
    // sudog(也称为唤醒原语,synchronization descriptor)是一种在 Golang 的运行时系统中用于实现 Goroutine 的等待和唤醒机制的数据结构
    waiting        *sudog         

    // 用于缓存 time.Sleep 操作所使用的定时器。
    // 为了避免每次调用 time.Sleep 都创建新的定时器,runtime 使用 timer 字段来缓存一个定时器,以便重复使用。
    // timer 结构体包含了与定时器相关的信息,如过期时间、触发时间等。
    timer          *timer        

    
    // Per-G GC state

    // gcAssistBytes字段的值为正数时,表示Goroutine具有一定的垃圾回收辅助信用,可以在不进行辅助工作的情况下继续分配内存。
    // 当Goroutine分配内存时,gcAssistBytes字段会递减。
    //  而当gcAssistBytes字段的值为负数时,表示Goroutine需要通过执行扫描工作来纠正负债。
    //  这意味着Goroutine需要帮助进行垃圾回收,以还清负债。
    gcAssistBytes int64

    ...
}



M: 代表工作线程,保存了自身使用的栈信息。

每个m都持有一个特殊的g0, 一个m最多可以持有10个锁。

// note 是运行时系统中用于协调和同步 Goroutine 的基本机制之一。
// 当一个 Goroutine 需要等待某个事件或条件满足时,它会被休眠在一个 note 上,直到被其他 Goroutine 唤醒。
type note struct {
    // Futex-based impl treats it as uint32 key,
    // while sema-based impl as M* waitm.
    // Used to be a union, but unions break precise GC.
    key uintptr
}


type m struct {
    //g0 字段存储了具有调度堆栈的 Goroutine 的信息。g0被称为工作线程(也叫内核线程)。
    // 执行用户 goroutine 代码时,使用用户 goroutine 自己的栈,因此调度时会发生栈的切换
    //它是一个指向 g 结构体的指针,该结构体包含了 Goroutine 的状态、堆栈指针、程序计数器等信息。
    //g0 是一个特殊的 Goroutine,它用于管理调度和协程的创建、销毁和切换。
    //通过使用 g0 字段,运行时系统可以管理和调度具有调度堆栈的 Goroutine。
    //它包含了关键的上下文信息,使得 Goroutine 能够在 M(线程)上正确地执行和切换
    g0 *g // goroutine with scheduling stack

    //表示当前正在执行的 Goroutine。curg 是一个指向 g 结构体的指针。在 M 上运行的 Goroutine 会存储在 curg 中。
    curg *g // current running goroutine

    // gsignal 字段是一个指向 g 结构体的指针,用于表示处理信号的 Goroutine。
    //在操作系统中,信号是用于通知进程发生了某个事件或需要进行某个操作的一种异步通知机制。
    //Golang 运行时系统中的 gsignal 字段用于保存负责处理信号的 Goroutine 的信息。
    //gsignal 字段存储了一个特殊的 Goroutine,它负责处理与运行时系统相关的信号。
    //当发生信号时,运行时系统会将信号传递给 gsignal 所指定的 Goroutine,并由该 Goroutine 进行相应的信号处理操作。
    //通过使用 gsignal 字段,Golang 的运行时系统可以将信号处理的责任委托给特定的 Goroutine,以便进行信号处理和相关操作。
    //这有助于将信号处理与其他 Goroutine 的执行分离,提高信号处理的可靠性和响应性。
    gsignal *g // signal-handling g

    //用于存储线程本地存储(TLS)的数据。
    //TLS 是一种机制,用于为每个线程提供独立的存储空间,使得每个线程都可以在其中存储和访问自己的数据, 而不会与其他线程的数据发生冲突。
    //tls 字段用于存储 TLS 的数据,以便每个线程可以访问自己的 TLS 数据。
    //在 m 结构体中,tls 字段是一个固定大小的数组,其中的元素类型为 uintptr。
    //每个元素可以存储一个指针或整数值,用于表示线程特定的数据。
    //通过使用 tls 字段,Golang 的运行时系统为每个线程提供了一个独立的存储空间,可以在其中存储线程特定的数据。
    //这有助于实现线程间的数据隔离和线程安全性。
    tls [tlsSlots]uintptr // thread-local storage (for x86 extern register)

    //M 的启动函数,表示在新创建的线程上运行的起始函数。在线程创建时,将执行此函数。
    //mstartfn 字段指向一个 func() 类型的函数,该函数作为 M 启动时的入口点。
    //它定义了 M 在启动时要执行的操作,通常是执行一些初始化工作,然后开始调度和执行 Goroutine
    mstartfn func()


    //用于表示与当前 M(Machine)相关联的 P(Processor)。
    //在 Golang 的并发模型中,P 是调度器(Scheduler)用于管理 Goroutine 的上下文的单位。
    //每个 P 都维护了一组 Goroutine 队列和相关的调度状态。p 字段用于存储与当前 M 相关联的 P 的地址。
    //puintptr 是一个 uintptr 类型的别名,它用于保存指向 P 的地址。
    //通过使用 p 字段,当前 M 可以知道它所关联的 P,并在需要时与该 P 进行交互。
    //如果 M 正在执行 Go 代码,则 p 字段将保存与之关联的 P 的地址。
    //如果 M 当前不在执行 Go 代码(例如在系统调用中),则 p 字段将为 nil。
    //通过使用 p 字段,Golang 的运行时系统可以将 M 与正确的 P 关联起来,
    //并确保 M 在执行 Go 代码时可以正确地与所属的 P 进行通信和调度。
    p puintptr // attached p for executing go code (nil if not executing go code)


    //表示 M 当前是否处于自旋状态。
    //自旋是一种等待的方式,当 M 需要等待一些条件满足时,会快速地尝试获取锁或资源,而不进行阻塞。
    spinning bool // m is out of work and is actively looking for work


    //park用于实现 Goroutine 的休眠(park)和唤醒操作。它是note类型。
    //note 是运行时系统中用于协调和同步 Goroutine 的基本机制之一。
    //当一个 Goroutine 需要等待某个事件或条件满足时,它会被休眠在一个 note 上,直到被其他 Goroutine 唤醒。
    //park 字段表示当前 M(Machine)所关联的 Goroutine 是否被休眠。
    //如果 park 字段的值为非零,则表示当前 Goroutine 处于休眠状态;如果 park 字段的值为零,则表示当前 Goroutine 是可运行的。
    //通过使用 park 字段,运行时系统可以对 Goroutine 进行休眠和唤醒的操作。
    //当 Goroutine 需要等待某个事件时,它会被休眠在相关的 note 上,直到事件发生。
    //当事件发生时,其他 Goroutine 可以通过操作相关的 note 来唤醒休眠的 Goroutine。
    park note

    // 用于存储当前 M(Machine)持有的锁的信息。
    //locksHeld 字段是一个长度为 10 的数组,每个元素都是 heldLockInfo 类型的结构体,
    //用于记录锁的详细信息,例如锁的地址、持有者 Goroutine 的信息等。
    //通过使用 locksHeld 字段,运行时系统可以跟踪和管理当前 M 持有的锁的信息,以便进行锁的获取、释放和调度。
    //锁的信息可以帮助运行时系统优化并发调度策略,避免死锁和竞态条件。
    locksHeld [10]heldLockInfo
    
    ...
    }


P: processor 的首字母,为 M 的执行提供“上下文”,保存 M 执行 G 时的一些资源

type p struct {
    // 在 allp 中的索引,通过 id 字段,可以在 allp 数组中快速定位到对应的 p 结构体,以进行处理器级别的操作和管理。
    //allp 是一个全局的 p 数组,存储了系统中所有的处理器(Processor)的信息。
    id int32


    //用于表示处理器(Processor)的状态。状态值有_Pidle、_Prunning、_Psyscall、_Pgcstop 和 _Pdead
    //_Pidle = 0    处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空;也有可能是几种状态正在过度中状态
    //_Prunning = 1 被线程 M 持有,并且正在执行用户代码或者调度器。只能由拥有当前P的M才可能修改此状态。M可以将P的状态修改为_Pidle(无工作可做)、_Psyscall(系统调用) 或 _Pgstop(GC); 另外M也可以P的使用权交给另一个M(调度一个锁定状态的G)
    //_Psyscall = 2 当前P没有执行用户代码,当前线程陷入系统调用
    //_Pgcstop =3   被线程 M 持有,当前处理器由于垃圾回收被停止,由 _Prunning 变为 _Pgcstop
    //_Pdead = 4     当前处理器已经不被使用,如通过动态调小 GOMAXPROCS 进行 P 收缩
    status uint32 // one of pidle/prunning/...


    // 每次调用 schedule 时会加一.用于调度器的统计和性能分析.
    //在 Golang 的调度器中,调度器会周期性地检查各个 P(Processor)的队列,并选择可执行的 Goroutine 进行调度。
    //每次进行调度时,都会增加 schedtick 字段的值,以记录调度操作的次数。
    schedtick uint32 // incremented on every scheduler call

    // 每次系统调用时加一.
    //Golang 的运行时系统可以跟踪和统计系统调用的次数,从而评估系统调用的开销和性能状况,并进行相应的优化和调整。
    syscalltick uint32 // incremented on every system call


    /mcache 是每个 M(Machine)与本地缓存相关的数据结构。它包含了与 M 关联的 Goroutine 执行时所需的本地分配缓存。
    //每个 M 都有自己的本地缓存(mcache)来加速 Goroutine 的内存分配和回收。
    //当 Goroutine 需要分配内存时,会首先检查关联的 M 的 mcache 是否为空。
    //如果不为空,则直接从 mcache 中获取内存进行分配。这样可以避免频繁地向全局堆申请内存,提高分配速度。
    //mcache 中还包含了分配和回收内存的一些其他信息,例如空闲对象列表、本地缓存的大小等。
    mcache *mcache

    // mspancache 是用于缓存 mspan 对象的字段,它表示从堆中缓存的 mspan 对象。
    //mspan 是用于管理内存分配的数据结构,每个 mspan 对象代表了一块内存页的管理信息。
    //mspancache 结构体包含以下字段:
    //mspancache 用于缓存 mspan 对象,以提高内存分配的性能。
    //当需要分配新的内存页时,运行时系统首先会尝试从 mspancache 中获取一个可用的 mspan 对象。
    //如果缓存中有可用的对象,就会将其分配给新的内存页。这样可以减少对堆的访问,提高内存分配的效率。
    //通过使用 mspancache,Golang 的运行时系统可以高效地管理和重用 mspan 对象,以提高内存分配和管理的性能。
    mspancache struct {
        // 一个整型字段,表示当前缓存中 mspan 对象的数量。
        len int
        //一个长度为 128 的 mspan 指针数组,用于存储 mspan 对象。
        buf [128]*mspan
    }


    // goidcache 和 goidcacheend 是用于缓存 Goroutine ID 的字段。
    //为了提高获取 Goroutine ID 的性能,runtime 包中维护了一个 Goroutine ID 的缓存。
    //这个缓存存储在 goidcache 和 goidcacheend 字段中。
    //goidcache 是一个 uint64 类型的字段,表示 Goroutine ID 缓存的起始值。
    //当程序需要获取 Goroutine ID 时,首先检查缓存中是否有可用的值。 如果有,就直接使用缓存中的值
    //goidcacheend 是一个 uint64 类型的字段,表示 Goroutine ID 缓存的结束值。
    //当缓存中的 Goroutine ID 达到 goidcacheend 时,需要重新获取新的 Goroutine ID 并更新缓存。
    //通过使用 Goroutine ID 缓存,可以减少对 runtime·sched.goidgen 的访问次数,从而提高获取 Goroutine ID 的效率。
    goidcache    uint64
    goidcacheend uint64

    // gFree 是一个用于管理空闲 Goroutine 的数据结构。
    //当一个 Goroutine完成执行(状态为 Gdead)并且不再需要继续使用时,它会被添加到 gFree 的链表中,以便后续可以被重新利用。
    //通过维护一个空闲 Goroutine 的链表,运行时系统可以节省创建和销毁 Goroutine 的开销,并且能够快速地获取可用的 Goroutine。
    //当需要创建新的 Goroutine时,运行时系统会首先尝试从 gFree 链表中获取一个空闲 Goroutine。
    //如果链表为空,那么会动态分配一个新的 Goroutine。
    //当一个 Goroutine完成执行后,它会被释放并添加到 gFree 链表中,以便下次需要时可以重复使用。
    //通过使用 gFree 数据结构,Golang 的运行时系统可以高效地管理和重用空闲的 Goroutine,
    //从而提高程序的并发性能和资源利用率。
    gFree struct {
        gList // 双向链表,用于存储空闲的 Goroutine
        n int32 //  表示空闲 Goroutine 的数量
    }


    // runqhead 和 runqtail 是用于表示可运行 Goroutine 队列的字段。
    //可运行 Goroutine 队列用于存储可立即执行的 Goroutine,即那些已经准备好被调度执行的 Goroutine。
    //runqhead 是一个 uint32 类型的字段,表示可运行 Goroutine 队列的头部位置。
    //当需要从可运行队列中获取 Goroutine 进行调度时,会从 runqhead 指定的位置开始获取。
    //runqtail 是一个 uint32 类型的字段,表示可运行 Goroutine 队列的尾部位置。
    //当有新的 Goroutine 准备好被调度时,会将其添加到 runqtail 指定的位置。
    //通过使用 runqhead 和 runqtail,运行时系统可以快速访问可运行 Goroutine 队列,实现高效的 Goroutine 调度。
    runqhead uint32
    runqtail uint32

    //runq 数组用于存储可运行 Goroutine 的指针。
    //guintptr 表示一个 g(Goroutine)的指针。当一个 Goroutine 准备好被调度时,它的指针会被添加到 runq 队列中。
    //每个P都有一个自己的runq,除了自身有的runq 还有一个全局的runq, 对于每个了解过GPM的gohper应该都知道这一点。
    //每个P下面runq的允许的最大goroutine 数量为256。
    runq [256]guintptr


    // runnext 非空时,代表的是一个 可运行状态的 G,
    // 这个 G 被 当前 G 修改为 ready 状态,相比 runq 中的 G 有更高的优先级。
    // 如果当前 G 还有剩余的可用时间,那么就应该运行这个 G
    // 运行之后,该 G 会继承当前 G 的剩余时间. 这个字段是用来实现调度器亲和性的,
    //当一个G阻塞时,这时P会再获取一个G进行绑定执行,
    //如果这时原来的G执行阻塞结束后,如果想再次被接着继续执行,就需要重新在P的 runq 进行排队,
    //当 runq 里有太多的goroutine 时将会导致这个刚刚被解除阻塞的G迟迟无法得到执行, 同时还有可能被其他处理器所窃取。
    //从 Go 1.5 开始得益于 P 的特殊属性,从阻塞 channel 返回的 Goroutine 会优先运行,
    //这里只需要将这个G放在 runnext 这个字段即可。
    runnext guintptr



    // timers 是一个用于存储定时器的数组,表示在某个特定时间要执行的操作。该字段用于实现标准库的 time 包。
    //访问 timers 字段时必须持有 timersLock 互斥锁,避免并发操作引起的竞态条件。
    timers []*timer

...
}

GMP 调度流程

协程调度器设计目的: 一来在用户态设计实现协程的管理(创建,运行,销毁),无需与操作系统内核线程交互,没有上下文切换的开销成本;二来通过程序设置协程初始化大小(eg:2kb), 能够减少线程大小占用内存,造成OOM的风险。 也尽可能的利用多核CPU的能力,将高并发目标设计思想应用到GMP中,包括tcmall内存分配mcache和P逻辑处理器的本地runq设计。

Goroutine的本质是让一组可复用函数运行在一组线程之上

用户态协程和OS的线程比例模型:(利用CPU多核)
image.png

GM调度模型的问题:

  1. 创建,销毁,调度g都需要每个M获取锁,容易形成激烈锁竞争,影响程序性能。
  2. 调度实现局部性差,从而导致额外的资源消耗。(eg: M创建G1, 此时M又要调度执行G2,G1则别调度到其他M)
  3. M的CPU切换频繁,还有优化空间。(M的任务划分,工作量公平问题不好处理)

P的加入带来影响?

image.png

image.png

  1. 多个本地队列P分担G的操作,将公共区间划分成多个独立的空间,能减少每个空间的锁竞争,利用多核CPU优势。
  2. 为了平衡多个P队列的任务,实现了work stealing算法。(工作窃取算法,当前p空闲,从其他p获取任务)
  3. 实现了handoff机制。(eg: 绑定M1的G1,进行syscall, 需要将M1与当前绑定的P解绑,让其他空闲的M绑定到当前P来处理,并将当前G1现场数据保存到gobuf等结构中,等待中断恢复继续执行)

Q: 为什么不直接将P实现在M上?

A: M的数量太多会导致本地队列管理变得复杂;M和P的执行机制不同,从降低耦合角度应该让P与M接偶。
将工作线程与调度逻辑耦合,如果工作线程的阻塞,通信会影响调度逻辑,影响调度任务性能;

image.png

其中:

  1. M最大值是10000,可以通过debug.SetMaxThreads()修改。
  2. runtime.schedule会在执行完61个本地goroutine之后,去全局队列g执行。
  3. 每个m在执行g的时候,会有时间限制,10ms。

GMP调度细节

  • 尽可能的复用系统线程M,避免频繁创建和销毁线程。
  • 利用多核并行能力,让同时处理的任务队列数量等于CPU核数
  • 任务窃取机制,M可以从其他M绑定的P的运行队列中窃取g执行。
  • handoff机制,M阻塞会将当前M上的P运行队列交给其他M执行。
  • 基于协作的抢占机制,每个g最多运行10ms就需要被抢占
  • Go1.14版本基于信号量抢占调度解决了GC和栈扫描无法被抢占问题。

GMP的优点?

  1. GMP模型可以实现高效的并发编程
  2. GMP模型可以实现动态的负载均衡。(不同M和P之间,过度空闲,负载情况下的任务合理调度)
  3. GMP模型可以实现公平的调度策略。(协作和信号的抢占机制,防止运行饥饿,长时间运行g情况)

GMP的缺点?

  1. 相对于传统线程模型,更加复杂和难理解。
  2. 可能会引入一些额外的开销和延迟。(内存分配,栈扫描,协程调度等)
  3. 协程泄漏等问题。

Goroutine的调度时机

  • 主动调度
  1. 代码中执行runtime.Gosched()
  2. Go 1.14版本之前无限for无法被抢占
  3. 先切g0, G与M解绑,G入全局队列
  • 被动调度
  1. 协程在休眠,阻塞(syscall,io,chan..),执行gc而暂停时让出执行机会。
  2. 被动调度的目的是保证最大化的利用CPU资源。
  3. 先切g0, G与M解绑,G不入全局队列
  • 抢占调度
  1. 系统监控定时监测,运行时间过长(>10ms)或者系统调用的协程被抢占。(每10ms监测一轮调度)
  2. 抢占时机:
  • P的本地队列中有待运行的G
  • 当前没有空闲的P和自旋的M
  • 当前系统调用的时间超过10ms

线程自旋(spinning Threads)

自旋的线程M的目的,是找一个可执行的G。 阻塞和唤醒的主体都是针对线程或者协程而言,对于golang调度角色则是M。

  1. 本质上是循环执行一个指定逻辑。(当前活跃的M获取一个待运行的G概率会高)
  2. 会浪费一定的CPU资源。
  3. 可以避免M的上下文切换。

如果P本地队列和全局队列都没有可执行的G,则当前M 有以下操作:

  • 选择让M进入阻塞状态,通过线程内核态与用户态切换唤醒。
  • 让M自旋,CPU空转等待可执行的G。
  • 自旋的线程数量不会超过GOMAXPROCS.

Work Stealing (工作窃取)

多线程工作计算的一种调度机制,是一种调度优化机制。

工作窃取: 当当前M1绑定的P1的本地队列中,没有可执行的G;并且全局队列中也无可执行的G的时候,会从其他M关联的P的本地队列中,获取部分可执行的G (1/2), 放入当前P的本地队列中。

目的

  1. 减少线程之间的负载不平衡,从而提高程序的并发性。
package main

import (
    "fmt"
    "math"
    "runtime"
    "sync"
    "time"
)

func init() {
    goMaxProcs := 2
        // 更新操作返回旧当前cpu默认核心数量
    cpuCoreNum := runtime.GOMAXPROCS(goMaxProcs)

    Ticker(func() {
        if goMaxProcs < cpuCoreNum {
            goMaxProcs += 1
            runtime.GOMAXPROCS(goMaxProcs)
            fmt.Println("goMaxProcs:", goMaxProcs)
        }
    }, time.Second*5)
}
func main() {

    var wg sync.WaitGroup
    for i := 0; i < 5000; i++ {
        wg.Add(1)
        go func() {
            x := 0
            for i := 0; i < math.MaxInt; i++ {
                x++
            }
            wg.Done()
        }()
    }

    wg.Wait()
}

func Ticker(f func(), d time.Duration) {
    go func() {
        ticker := time.NewTicker(d)
        for {
            select {
            case <-ticker.C:
                go f()
            }
        }
    }()
}

操作执行: GODEBUG=schedtrace=1000 go run main.go

可以看到不同时刻,全局p的数量,和不同p本地队列G,全局队列中G的数量。每个p本地g数量max=256。
image.png

GMP调度过程中的阻塞

var waitReasonStrings = [...]string{
    waitReasonZero:                  "",
    waitReasonGCAssistMarking:       "GC assist marking",
    waitReasonIOWait:                "IO wait",
    waitReasonChanReceiveNilChan:    "chan receive (nil chan)",
    waitReasonChanSendNilChan:       "chan send (nil chan)",
    waitReasonDumpingHeap:           "dumping heap",
    waitReasonGarbageCollection:     "garbage collection",
    waitReasonGarbageCollectionScan: "garbage collection scan",
    waitReasonPanicWait:             "panicwait",
    waitReasonSelect:                "select",
    waitReasonSelectNoCases:         "select (no cases)",
    waitReasonGCAssistWait:          "GC assist wait",
    waitReasonGCSweepWait:           "GC sweep wait",
    waitReasonGCScavengeWait:        "GC scavenge wait",
    waitReasonChanReceive:           "chan receive",
    waitReasonChanSend:              "chan send",
    waitReasonFinalizerWait:         "finalizer wait",
    waitReasonForceGCIdle:           "force gc (idle)",
    waitReasonSemacquire:            "semacquire",
    waitReasonSleep:                 "sleep",
    waitReasonSyncCondWait:          "sync.Cond.Wait",
    waitReasonTimerGoroutineIdle:    "timer goroutine (idle)",
    waitReasonTraceReaderBlocked:    "trace reader (blocked)",
    waitReasonWaitForGCCycle:        "wait for GC cycle",
    waitReasonGCWorkerIdle:          "GC worker (idle)",
    waitReasonPreempted:             "preempted",
    waitReasonDebugCall:             "debug call",
}

整体阻塞分为几大类:

  1. 发生系统调用syscall
  2. 内存同步访问(channel读写操作,Mutex与读写锁,select,sync.waitGroup同步原语等)
  3. 垃圾回收GC
  4. 睡眠sleep

在Go语言的GMP调度模型中,G代表Goroutine,M代表线程(Machine),P代表逻辑处理器。当一个M执行的G执行系统调用并阻塞时,M会尝试将当前的G放入系统调用队列,然后尝试从P的本地队列或者全局队列中获取新的G来执行。以下是这个过程的详细描述:

M绑定的P,执行G,发生syscall, M与G解绑,M继续执行P中runq中其他G,若没有,执行全局队列中G;若还是没有,执行其他P的本地runq中的G。

  • 系统调用阻塞:当一个G执行系统调用并阻塞时,M会尝试将该G移动到系统调用队列中。
  • 寻找新G:在将G移动到系统调用队列后,M会检查它当前绑定的P的本地队列,看是否有其他可执行的G。
  • 本地队列为空:如果本地队列为空,M会尝试从全局队列中获取G来执行。
  • 绑定其他P:如果全局队列也没有G,M可能会尝试与当前的P解绑,并绑定到另一个P上,以尝试从那个P的本地队列中获取G。
  • 空闲M:如果所有P的本地队列和全局队列都没有G,M会变成空闲状态,等待新的G出现或者被调度执行其他任务。
  • 系统调用完成:当系统调用完成时,之前阻塞的G会被重新放回P的本地队列,等待M再次执行。
  • M的重新调度:如果M在空闲时被重新调度,它会再次尝试从绑定的P的本地队列或全局队列中获取G来执行。
    这个过程确保了Go运行时能够高效地利用线程资源,即使在某些Goutine因为系统调用而阻塞的情况下,也能继续执行其他任务,从而提高程序的整体性能。

协程的状态

一个活动中的协程可以处于两个状态:运行状态和阻塞状态。

一个处于睡眠中的(time.sleep)或者等待syscall返回的协程被认为是运行状态。

image.png

Coin Marketplace

STEEM 0.15
TRX 0.12
JST 0.025
BTC 55819.41
ETH 2522.74
USDT 1.00
SBD 2.32