当前位置: 首页 > news >正文

一文解析golang中的协程与GMP模型

文章目录

  • 前言
  • 1、线程实现模型
    • 1.1、用户级线程与内核级线程
    • 1.2、内核级线程模型
    • 1.3、用户级线程模型
    • 1.3、两级线程模型
  • 2、GMP模型
    • 2.1、GMP模型概述
    • 2.1、GMP v1版本 - GM模型
    • 2.2、GMP v2版本 - GMP模型
    • 2.3、GMP相关源码
    • 2.4 调度流程
    • 2.5 设计思想
  • 3.总结


前言

并发(并行)一直是在编程领域中一直受关注的一个核心主题。而go语言在诞生之初就带着「 高并发 」的明星光环,进入开发者的视野。而golang的高并发则是由goroutine与GMP调度模型实现的,因此探究goroutine与GMP的实变得尤为重要。


1、线程实现模型

在聊聊goroutine之前,先来聊一下线程的实现模型,因为goroutine的实现也是基于线程的实现模型,而更好的探究goroutine属于哪一种。

简要的来说线程的实现模型总共有一下三种:

  • 内核级线程模型
  • 用户级线程模型
  • 两级线程模型(也称混合型线程模型)

它们之间最大的差异就在于用户线程与内核调度实体(KSE,Kernel Scheduling Entity)之间的对应关系上。而KSE则是内核级线程,是操作系统内核的最小调度单元。 一般所说的协程都是用户级的线程模型,虽然goroutine被称作go语言版本实现的协程,但是它其实是第三种:两级线程模型

1.1、用户级线程与内核级线程

进程是操作系统中资源分配和拥有的单位,同一个进程内的线程共享进程的资源线程是处理器调度的基本单位在同一进程中,同一个进程中线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。

而根据操作系统内核是否对线程可感知,可以把线程分为内核线程和用户线程

  • 用户级线程: 由应用程序所支持的线程实现, 内核意识不到用户级线程的实现
  • 内核级线程: 内核级线程又称为内核支持的线程

1.2、内核级线程模型

在内核级线程模型中,用户线程与内核线程KSE是一对一(1:1)的映射模型。一个用户线程绑定一个实际的内核线程。内核线程建立和销毁都是由操作系统负责、通过系统调用完成的。在内核的支持下运行,无论是用户进程的线程,或者是系统进程的线程,他们的创建、撤销、切换都是依靠内核实现的。所以也因此内核级线程模型的特点是:当某个线程希望创建一个新线程或撤销一个已有线程时,它会进行一个系统调用。
在这里插入图片描述

  • Java SE最常用的JVM是Oracle/Sun研发的HotSpot VM,而在这个JVM上基本默认的就是1:1的内核线程模型,除了Solaris上可以支持M:N,以及C++11的 std::thread也是,都是对操作系统的线程的一层封装,创建出来的线程再与每个KSE进行静态绑定。

  • 优点: 借助操作系统内核的线程及调度器,实现相对用户线程模型简单,cpu可以快速切换调度线程。在多处理器的情况下多个线程可以同时运行,相较于用户线程模型真正实现了并行处理。

  • 缺点: 由于直接借助了操作系统内核来创建、销毁和以及多个线程之间的上下文切换和调度,因此资源成本大幅上涨,对性能影响大。(用户态切换成内核态)。

1.3、用户级线程模型

在用户级线程模型中,用户线程与内核线程KSE是一对一(N:1)的映射模型。多个用户线程的一般从属于单个进程并且多线程的调度是由用户自己的线程库来完成,线程的创建、销毁以及多线程之间的协调等操作都是由用户自己的线程库来负责而无须借助系统调用来实现。一个进程中所有创建的线程都只和同一个 KSE 在运行时动态绑定,也就是说,操作系统只知道用户进程而对其中的线程是无感知的,内核的所有调度都是基于用户进程。
在这里插入图片描述

  • 很多语言的协程库都是以此模型进行实现的,如Java的Quasar,python的gevent,js的async/await和Promise。
  • 优点:由于线程调度是在用户层面完成的,所以对于不需要进行内核态与用户态进行切换。大量减少了上下文的开销,这种实现方式也比内核级线程更加的轻量,对系统资源的消耗会小很多。
  • 缺点不能做到真正意义的并发,一个线程的 I/O 操作会导致整个进程被挂起的主要原因是因为用户线程的调度和管理是由用户空间的库或运行时环境来完成,而不涉及操作系统内核。因此,操作系统并不了解用户线程的存在,也无法对用户线程的 I/O 操作进行直接管理和调度。当一个用户线程发起了一个阻塞式的 I/O 操作(如读取文件、网络通信等),由于用户线程模型的特性,这个线程会被阻塞,无法继续执行。由于操作系统无法感知到这种阻塞,因此整个进程也会被挂起,因为操作系统认为进程中的所有线程都无法继续执行。

1.3、两级线程模型

在两级线程模型中,用户线程与内核线程KSE是多对多(N:M)的映射模型。 吸收了用户级线程与内核级线程的优点,一个进程内的多个线程可以分别绑定一个自己的KSE,这点与内核级线程模型相似,但是用户线程并不与KSE唯一绑定,可以多个用户线程映射到同一个KSE。当某个KSE因为其绑定的线程的阻塞操作被内核调度出CPU时,其关联的进程中其余用户线程可以重新与其他KSE绑定运行。 两级代表的是用户调度器实现用户线程到KSE的调度,而内核调度器实现KSE到cpu上的调度。
在这里插入图片描述
因此两级线程模型既不是用户级线程模型完全靠自己调度,也不是内核级线程模型完全靠操作系统调度,而是自身调度与系统调度协同进行。但因为这种模型高度复杂,所以更多作为第三方库进行形式。Go语言中的runtime调度器就是采用的这种实现方案。实现了Goroutine与KSE的动态关联。

2、GMP模型

2.1、GMP模型概述

OS 线程都有一个固定大小的内存块(一般会是 2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。因为操作系统线程是由操作系统内核管理和调度的,因此需要更多的内存来存储线程的状态信息、调度信息等。而 Go 的 goroutine 是由 Go 运行时环境(runtime)自行管理和调度的,因此2MB的初始内存对goroutine又显得太大,但是对于复杂的递归计算的任务又会显得太小。因此go语言则设计了自己的线程:gorountine。

在 Go 语言中,每一个 goroutine 是一个独立的执行单元,相较于每个 OS 线程固定分配 2M 内存的模式,goroutine 的栈采取了动态扩容方式, 初始时仅为 2KB,随着任务执行按需增长,最大可达 1GB(64 位机器最大是 1G,32 位机器最大是 256M),且完全由 golang 自己的调度器 Go Scheduler 来调度。此外,GC 还会周期性地将不再使用的内存回收,收缩栈空间。 因此,Go 程序可以同时并发成千上万个 goroutine 是得益于它强劲的调度器和高效的内存模型。

在GMP中:

  • G: 表示 Goroutine,每个 Goroutine 对应一个 G 结构体,G 存储 Goroutine 的运行堆栈、状态以及任务函数,可重用。G 并非执行体,每个 G 需要绑定到 P 才能被调度执行。
  • P: Processor,表示逻辑处理器, 对 G 来说,P 相当于 CPU 核,G 只有绑定到 P(在 P 的 local runq 中)才能被调度。对 M 来说,P 提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等,P 的数量决定了系统内最大可并行的 G 的数量(前提:物理 CPU 核数 >= P 的数量),P 的数量由用户设置的 GOMAXPROCS 决定,但是不论 GOMAXPROCS 设置为多大,P 的数量最大为 256,而默认的为CPU的核数。
  • M: Machine,OS 线程抽象,代表着真正执行计算的资源,在绑定有效的 P 后,进入 schedule 循环;而 schedule 循环的机制大致是从 Global 队列、P 的 Local 队列以及 wait 队列中获取 G,切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 M,如此反复。M 并不保留 G 状态,这是 G 可以跨 M 调度的基础,M 的数量是不定的,由 Go Runtime 调整,为了防止创建过多 OS 线程导致系统调度不过来,目前默认最大限制为 10000 个。

用户线程最终肯定都是要交由 OS 线程来执行的,goroutine(称为 G)也不例外,但是 G 并不直接绑定 OS 线程运行,而是由 Goroutine Scheduler 中的 P - Logical Processor (逻辑处理器)来作为两者的『中介』,P 可以看作是一个抽象的资源或者一个上下文,一个 P 绑定一个 OS 线程,在 golang 的实现里把 OS 线程抽象成一个数据结构:M,G 实际上是由 M 通过 P 来进行调度运行的,但是在 G 的层面来看,P 提供了 G 运行所需的一切资源和环境,因此在 G 看来 P 就是运行它的 “CPU”,由 G、P、M 这三种由 Go 抽象出来的实现。也因此从这可以看出来GMP模型就是两级线程模型,而p就是内核调度器+用户调度器结合的中间态

2.1、GMP v1版本 - GM模型

在早期的版本中(1.1前),并不是GMP模型,而是GM模型,GM模型可以看做用户线程模型。
在这里插入图片描述
M 想要执行、放回 G 都必须访问全局 G 队列,并且 M 有多个,即多线程访问同一资源需要加锁进行保证互斥 / 同步,所以全局 G 队列是有互斥锁进行保护的。因此GM模型拥有以下几个缺点:

  • 单一全局互斥锁(Sched.Lock)和集中状态存储的存在导致所有 goroutine 相关操作,比如:创建、重新调度等都要上锁
  • goroutine 传递问题:M 经常在 M 之间传递『可运行』的 goroutine,这导致调度延迟增大以及额外的性能损耗
  • 每个 M 做内存缓存,导致内存占用过高,数据局部性较差;
  • 由于 syscall 调用而形成的剧烈的 worker thread 阻塞和解除阻塞,导致额外的性能损耗

2.2、GMP v2版本 - GMP模型

因此golang后面基于work-stealing算法,在GM模型的基础上加入了P,分别去协调G与M。

work-stealing调度算法:

  • 每个 P 维护一个 G 的本地队列;
  • 当一个 G 被创建出来,或者变为可执行状态时,就把他放到 P 的可执行队列中;
  • 当一个 G 在 M 里执行结束后,P 会从队列中把该 G 取出;如果此时 P 的队列为空,即没有其他 G 可以执行, P就随机选择另外一个 P,从其可执行的 G 队列中取走一半。

整体的调度模型如下图:
在这里插入图片描述
当通过 go 关键字创建一个新的 goroutine 的时候,它会优先被放入 P 的本地队列。为了运行 goroutine,M 需要持有(绑定)一个 P,接着 M 会启动一个 OS 线程,循环从 P 的本地队列里取出一个 goroutine 并执行。当 M 执行完了当前 P 的 Local 队列里的所有 G 后,P的本地队列会从全局队列中取一个出来。而全局队列也空了的时候则会像上文所说的work-stealing算法,P会从别的P的本地队列steal gorountine来进行任务。

P.runnext是什么?
每个P上都有一个runnext字段,类型是guintptr,语义为下一个优先执行的goroutine但是只能存储一个,如果有多个,被抢占掉的goroutine会被放到可执行队列的队尾,继续等待正常调度。

//$GOROOT/src/runtime/proc.go`// runqput tries to put g on the local runnable queue.
// If next is false, runqput adds g to the tail of the runnable queue.
// If next is true, runqput puts g in the _p_.runnext slot.
// If the run queue is full, runnext puts g on the global queue.
// Executed only by the owner P.
func runqput(_p_ *p, gp *g, next bool) {if next {retryNext:oldnext := _p_.runnext// 将 _p_.runnext 的旧值和当前 goroutine 进行交换if !_p_.runnext.cas(oldnext, guintptr(unsafe.Pointer(gp))) {goto retryNext}// 如果 _p_.runnext 的旧值为空,则直接返回if oldnext == 0 {return}// Kick the old runnext out to the regular run queue.// 如果 _p_.runnext 的旧值不为空,获取其对应的 goroutine 指针,gp 为_p_.runnext 的旧值gp = oldnext.ptr()}
// 如果 next 为 false ,则 gp 仍为新创建的 goroutine
// 如果 next 为 true ,则 gp 为_p_.runnext 的旧值
// 将 gp 放置到 P 本地队列的队尾,如果 P 的本地队列已满,则放置到全局队列上,等待调度器进行调度
retry:h := atomic.LoadAcq(&_p_.runqhead) // load-acquire, synchronize with consumerst := _p_.runqtailif t-h < uint32(len(_p_.runq)) {_p_.runq[t%uint32(len(_p_.runq))].set(gp)atomic.StoreRel(&_p_.runqtail, t+1) // store-release, makes the item available for consumptionreturn}if runqputslow(_p_, gp, h, t) {return}// the queue is not full, now the put above must succeedgoto retry
} 

2.3、GMP相关源码

源码版本为: go sdk-1.21.5

这部分的源码主要涉及三个文件:

  • runtime/amd_64.s 涉及到进程启动以及对CPU执行指令进行控制的汇编代码,进程的初始化部分也在这里面
  • runtime/runtime2.go 这里主要是运行时中一些重要数据结构的定义,比如g、m、p以及涉及到接口、defer、panic、map、slice等核心类型
  • runtime/proc.go 一些核心方法的实现,涉及gmp调度等核心代码在这里

G的结构体:

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 //go:systemstack 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).// (记录协程栈的栈顶和栈底位置)stack stack // offset known to runtime/cgo// (主要作用是参与一些比较计算,当发现容量要超过栈分配空间后,可以进行扩容或者收缩)stackguard0 uintptr // offset known to liblinkstackguard1 uintptr // offset known to liblink_panic *_panic // 最内层的panic_defer *_defer // 最内层的defer函数m         *m // current m; offset known to arm liblink,当前g绑定的m(逻辑处理器)sched     gobufsyscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gcsyscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gcstktopsp  uintptr // expected sp at top of stack, to check in traceback// 用于做参数传递,睡眠时其他goroutine可以设置param,唤醒时该g可以读取这些param// in four ways:// 1. When a channel operation wakes up a blocked goroutine, it sets param to//    point to the sudog of the completed blocking operation.// 2. By gcAssistAlloc1 to signal back to its caller that the goroutine completed//    the GC cycle. It is unsafe to do so in any other way, because the goroutine's//    stack may have moved in the meantime.// 3. By debugCallWrap to pass parameters to a new goroutine because allocating a//    closure in the runtime is forbidden.// 4. When a panic is recovered and control returns to the respective frame,//    param may point to a savedOpenDeferState.param unsafe.Pointer// 记录当前gorountine的状态atomicstatus atomic.Uint32stackLock    uint32 // sigprof/scang lock; TODO: fold in to atomicstatus// gorountine 唯一的标识goid       uint64schedlink  guintptrwaitsince  int64      // approx time when the g become blockedwaitreason waitReason // if status==Gwaiting// 标记是否可以呗抢占preempt       bool // preemption signal, duplicates stackguard0 = stackpreemptpreemptStop   bool // transition to _Gpreempted on preemption; otherwise, just deschedulepreemptShrink bool // shrink stack at synchronous safe point// asyncSafePoint is set if g is stopped at an asynchronous// safe point. This means there are frames on the stack// without precise pointer information.asyncSafePoint boolpaniconfault bool // panic (instead of crash) on unexpected fault addressgcscandone   bool // g has scanned stack; protected by _Gscan bit in statusthrowsplit   bool // must not split stack// activeStackChans indicates that there are unlocked channels// pointing into this goroutine's stack. If true, stack// copying needs to acquire channel locks to protect these// areas of the stack.activeStackChans bool// parkingOnChan indicates that the goroutine is about to// park on a chansend or chanrecv. Used to signal an unsafe point// for stack shrinking.parkingOnChan atomic.Bool// inMarkAssist indicates whether the goroutine is in mark assist.// Used by the execution tracer.inMarkAssist boolcoroexit     bool // argument to coroswitch_mraceignore    int8  // ignore race detection eventsnocgocallback bool  // whether disable callback from Ctracking      bool  // whether we're tracking this G for sched latency statisticstrackingSeq   uint8 // used to decide whether to track this GtrackingStamp int64 // timestamp of when the G last started being trackedrunnableTime  int64 // the amount of time spent runnable, cleared when running, only used when trackinglockedm       muintptrsig           uint32writebuf      []bytesigcode0      uintptrsigcode1      uintptrsigpc         uintptrparentGoid    uint64          // goid of goroutine that created this goroutinegopc          uintptr         // pc of go statement that created this goroutineancestors     *[]ancestorInfo // ancestor information goroutine(s) that created this goroutine (only used if debug.tracebackancestors)startpc       uintptr         // pc of goroutine functionracectx       uintptrwaiting       *sudog         // sudog structures this g is waiting on (that have a valid elem ptr); in lock ordercgoCtxt       []uintptr      // cgo traceback contextlabels        unsafe.Pointer // profiler labelstimer         *timer         // cached timer for time.SleepsleepWhen     int64          // when to sleep untilselectDone    atomic.Uint32  // are we participating in a select and did someone win the race?coroarg *coro // argument during coroutine transfers// goroutineProfiled indicates the status of this goroutine's stack for the// current in-progress goroutine profilegoroutineProfiled goroutineProfileStateHolder// Per-G tracer state.trace gTraceState// Per-G GC state// gcAssistBytes is this G's GC assist credit in terms of// bytes allocated. If this is positive, then the G has credit// to allocate gcAssistBytes bytes without assisting. If this// is negative, then the G must correct this by performing// scan work. We track this in bytes to make it fast to update// and check for debt in the malloc hot path. The assist ratio// determines how this corresponds to scan work debt.gcAssistBytes int64
}

其中比较关键的字段有:

  • stack: 描述了当前 Goroutine 的栈内存范围 [stack.lo, stack.hi);
  • stackguard0: 可以用于调度器抢占式调度;preempt,preemptStop,preemptShrink跟抢占相关;
  • _defer 和 _panic: 分别记录这个 G 最内侧的panic和 _defer结构体;
  • m: 记录当前 G 占用的线程 M,可能为空;
  • atomicstatus: 表示G 的状态;
  • sched: 存储 G 的调度相关的数据;
  • goid: 表示 G 的 ID,对开发者不可见;

Go语言刻意没有提供goid的原因是为了避免被滥用。因为大部分用户在轻松拿到goid之后,在之后的编程中会不自觉地编写出强依赖goid的代码。强依赖goid将导致这些代码不好移植,同时也会导致并发模型复杂化。同时,Go语言中可能同时存在海量的Goroutine,但是每个Goroutine何时被销毁并不好实时监控,这也会导致依赖goid的资源无法很好地自动回收(需要手工回收)。——《Go语言高级编程》

以及一个相关的字段runtime.gobuf结构体:

type gobuf struct {sp   uintptr      // 栈指针pc   uintptr      // 程序计数器,记录G要执行的下一条指令位置g    guintptr     // 持有 runtime.gobuf 的 Gret  uintptr      // 系统调用的返回值......
}

而GMP说的G则是来自于这里,但是这里的G的分类却不仅仅是在GMP中,除GMP协程以外还包括了:

  • sysmon协程: sysmon协程也是runtime的一部分,sysmon协程直接运行在M不需要P,主要做一些检查工作如:检查死锁、检查计时器获取下一个要被触发的计时任务、检查是否有ready的网络调用以恢复用户G的工作、检查一个G是否运行时间太长进行抢占式调度。
  • g0协程:G0 主要负责调度和执行用户级线程,包括创建、销毁和切换用户级线程,以及管理用户级线程的执行。
    • 每次启动一个m,都会第一个创建的goroutine,就是g0(g0不是整个进程唯一的,而是一个线程中唯一的;
    • g0比较特殊, 每一个m都会有且只有一个自己的g0, g0不指向任何可执行的函数;g0是一个全局的变量,定义在 runtime.g0中,g0分配在系统栈中,通过汇编赋值完成,拥有着比较大的栈空间,而其他的g,则分配在用户栈中,初始化只有2K的栈空间,通过newg函数分配;
    • g0仅用于负责调度其他的g(m可能会有很多的g,然后g0用来保持调度栈的信息,当一个m执行从g1切换到g2,首先应该切换到g0,通过g0把g1干掉,把g2加进来 , 即 g0 就是其他g的指挥官,起着桥梁的作用。

创建G的过程:

  • 当我们使用go关键字新建一个goroutine时,编译器会编译为runtime中对应的函数调用(newproc,而go 关键字后面的函数成为协程的任务函数),进行创建:
// Create a new g running fn.
// Put it on the queue of g's waiting to run.
// The compiler turns a go statement into a call to this.
func newproc(fn *funcval) {// 获取当前的 goroutine(G)gp := getg()// 获取调用者的程序计数器(program counter)pc := getcallerpc()// 调用 systemstack 函数,该函数用于在系统栈上执行指定的函数。在这里,它用于执行后续的 goroutine 创建和调度操作systemstack(func() {// systemstack 中,调用 newproc1 函数创建一个新的 goroutine,并将其放入等待运行的队列中。newproc1 函数用于执行实际的 goroutine 创建操作newg := newproc1(fn, gp, pc, false, waitReasonZero)// 获取当前的 P(处理器),并调用 runqput 将新创建的 goroutine 放入等待运行的队列中pp := getg().m.p.ptr()runqput(pp, newg, true)// 如果 mainStarted 为真(表示主函数已经开始执行),则调用 wakep 函数唤醒等待的 Pif mainStarted {wakep()}})
}

而其中具体的newproc1的代码流程为:

// Create a new g in state _Grunnable (or _Gwaiting if parked is true), starting at fn.
// callerpc is the address of the go statement that created this. The caller is responsible
// for adding the new g to the scheduler. If parked is true, waitreason must be non-zero.
func newproc1(fn *funcval, callergp *g, callerpc uintptr, parked bool, waitreason waitReason) *g {if fn == nil {fatal("go of nil func value")}// 获取当前的 M(操作系统线程),并将其保存在局部变量 mp 中。这一步禁用抢占,因为在本地变量中持有了 M 和 Pmp := acquirem() // disable preemption because we hold M and P in local vars.// 获取当前的 P(处理器),并尝试从全局的空闲 G 列表中获取一个可用的 G 对象。如果获取不到,//则通过 malg 函数创建一个新的 G 对象,并将其状态设置为 _Gidlepp := mp.p.ptr()newg := gfget(pp)if newg == nil {newg = malg(stackMin)// 初始化完成后设置为 _Gdeadcasgstatus(newg, _Gidle, _Gdead)allgadd(newg) // publishes with a g->status of Gdead so GC scanner doesn't look at uninitialized stack.}// 确保新创建的 G 对象的栈不为空,如果为空则抛出异常if newg.stack.hi == 0 {throw("newproc1: newg missing stack")}if readgstatus(newg) != _Gdead {throw("newproc1: new g is not Gdead")}// 为新创建的 G 对象分配栈空间,并设置栈指针、调用栈帧、函数指针等属性totalSize := uintptr(4*goarch.PtrSize + sys.MinFrameSize) // extra space in case of reads slightly beyond frametotalSize = alignUp(totalSize, sys.StackAlign)sp := newg.stack.hi - totalSizeif usesLR {// caller's LR*(*uintptr)(unsafe.Pointer(sp)) = 0prepGoExitFrame(sp)}if GOARCH == "arm64" {// caller's FP*(*uintptr)(unsafe.Pointer(sp - goarch.PtrSize)) = 0}memclrNoHeapPointers(unsafe.Pointer(&newg.sched), unsafe.Sizeof(newg.sched))newg.sched.sp = spnewg.stktopsp = spnewg.sched.pc = abi.FuncPCABI0(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same functionnewg.sched.g = guintptr(unsafe.Pointer(newg))gostartcallfn(&newg.sched, fn)// 设置新创建的 G 对象的父 GID、调用者 PC、起始 PC 等属性,并根据是否为系统 goroutine 进行相应的处理newg.parentGoid = callergp.goidnewg.gopc = callerpcnewg.ancestors = saveAncestors(callergp)newg.startpc = fn.fnif isSystemGoroutine(newg, false) {sched.ngsys.Add(1)} else {// Only user goroutines inherit pprof labels.if mp.curg != nil {newg.labels = mp.curg.labels}if goroutineProfile.active {// A concurrent goroutine profile is running. It should include// exactly the set of goroutines that were alive when the goroutine// profiler first stopped the world. That does not include newg, so// mark it as not needing a profile before transitioning it from// _Gdead.newg.goroutineProfiled.Store(goroutineProfileSatisfied)}}// Track initial transition?newg.trackingSeq = uint8(cheaprand())if newg.trackingSeq%gTrackingPeriod == 0 {newg.tracking = true}gcController.addScannableStack(pp, int64(newg.stack.hi-newg.stack.lo))// Get a goid and switch to runnable. Make all this atomic to the tracer.trace := traceAcquire()var status uint32 = _Grunnableif parked {status = _Gwaitingnewg.waitreason = waitreason}casgstatus(newg, _Gdead, status)if pp.goidcache == pp.goidcacheend {// Sched.goidgen is the last allocated id,// this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].// At startup sched.goidgen=0, so main goroutine receives goid=1.pp.goidcache = sched.goidgen.Add(_GoidCacheBatch)pp.goidcache -= _GoidCacheBatch - 1pp.goidcacheend = pp.goidcache + _GoidCacheBatch}// 分配gidnewg.goid = pp.goidcachepp.goidcache++newg.trace.reset()if trace.ok() {trace.GoCreate(newg, newg.startpc, parked)traceRelease(trace)}// 如果启用了竞争检测,设置竞争检测上下文,并进行相关的处理.if raceenabled {newg.racectx = racegostart(callerpc)newg.raceignore = 0if newg.labels != nil {// See note in proflabel.go on labelSync's role in synchronizing// with the reads in the signal handler.racereleasemergeg(newg, unsafe.Pointer(&labelSync))}}// 释放之前获取的 Mreleasem(mp)return newg
}

具体的流程注释补充到代码中,总结一下:newproc1 函数用于创建一个新的 goroutine,并初始化其相关属性,以便将其添加到调度器中等待执行。这个函数涉及了对 goroutine 的栈空间、调度状态、跟踪信息和竞争检测等方面的处理,确保了新创建的 goroutine 能够被正确地加入到调度器中,并在适当的时机被调度执行。

补充一下代码中出现的几个go的流程状态

  • _Gidle:代表协程刚开始创建时的状态,当新创建的协程初始化后,为变为_Gdead状态
  • _Gwaiting:代表协程正在运行队列中,等待被运行
  • _Grunnable: 表示当前协程在运行时被锁定,陷入阻塞,不能执行用户代码
  • _Gsyscall: 当前 G 正在被系统调用;
  • _ Grunning:当前 G 正在被运行;
  • _Gdead: 初始化后协程的状态/协程被销毁后的状态。

而第一个状态流转到_Gdead的原因是为了以确保新创建的 G 对象处于一个初始的空闲状态,同时避免了在初始化过程中因为状态不一致而导致的问题。这样的状态转换也可以避免在初始化过程中触发 GC 扫描等操作,因为处于 _Gdead 状态的 G 对象不会被 GC 扫描器处理。

  • G从创建到结束的生命周期中经历的各种状态变化过程:
    在这里插入图片描述

M的结构体:

type m struct {// 每个m绑定的一个对应的g0线程,用来调度g0      *g     // goroutine with scheduling stackmorebuf gobuf  // gobuf arg to morestackdivmod  uint32 // div/mod denominator for arm - known to liblink_       uint32 // align next field to 8 bytes// Fields not known to debuggers.procid     uint64       // for debuggers, but offset not hard-codedgsignal    *g           // signal-handling ggoSigStack gsignalStack // Go-allocated signal handling stacksigmask    sigset       // storage for saved signal mask// 线程的本地存储,可以从任意时刻访问当前线程上的协程g、结构体m、逻辑处理器p等tls      [tlsSlots]uintptr // thread-local storage (for x86 extern register)mstartfn func()// 正在m上运行的gcurg      *g       // current running goroutinecaughtsig guintptr // goroutine running during fatal signal// 当前m上绑定的pp         puintptr // attached p for executing go code (nil if not executing go code)nextp     puintptroldp      puintptr // the p that was attached before executing a syscallid        int64mallocing int32throwing  throwType// 用于让当前m上的协程继续运行,禁止被抢占,保证curg的运行preemptoff string // if != "", keep curg running on this mlocks      int32dying      int32profilehz  int32// spinning为true代表当前m正在自旋寻找work// 会先检查本地队列,然后全局队列,然后检查network poller尝试执行一些网络 I/O 相关的任务,m则会可能发生阻塞,并尝试gc// 或者偷一点任务,如果都没有则会休眠spinning      bool // m is out of work and is actively looking for workblocked       bool // m is blocked on a notenewSigstack   bool // minit on C thread called sigaltstackprintlock     int8incgo         bool          // m is executing a cgo callisextra       bool          // m is an extra misExtraInC    bool          // m is an extra m that is not executing Go codeisExtraInSig  bool          // m is an extra m in a signal handlerfreeWait      atomic.Uint32 // Whether it is safe to free g0 and delete m (one of freeMRef, freeMStack, freeMWait)needextram    booltraceback     uint8ncgocall      uint64        // number of cgo calls in totalncgo          int32         // number of cgo calls currently in progresscgoCallersUse atomic.Uint32 // if non-zero, cgoCallers in use temporarilycgoCallers    *cgoCallers   // cgo traceback if crashing in cgo call// 没有gorountine需要运行,休眠在park上park note// 记录所有工作线程的一个链表alllink     *m // on allmschedlink   muintptrlockedg     guintptrcreatestack [32]uintptr // stack that created this thread, it's used for StackRecord.Stack0, so it must align with it.lockedExt   uint32      // tracking for external LockOSThreadlockedInt   uint32      // tracking for internal lockOSThreadnextwaitm   muintptr    // next m waiting for lockmLockProfile mLockProfile // fields relating to runtime.lock contention// wait* are used to carry arguments from gopark into park_m, because// there's no stack to put them on. That is their sole purpose.waitunlockf          func(*g, unsafe.Pointer) boolwaitlock             unsafe.PointerwaitTraceBlockReason traceBlockReasonwaitTraceSkip        intsyscalltick uint32freelink    *m // on sched.freemtrace       mTraceState// these are here because they are too large to be on the stack// of low-level NOSPLIT functions.libcall    libcalllibcallpc  uintptr // for cpu profilerlibcallsp  uintptrlibcallg   guintptrwinsyscall winlibcall // stores syscall parameters on windowsvdsoSP uintptr // SP for traceback while in VDSO call (0 if not in call)vdsoPC uintptr // PC for traceback while in VDSO call// preemptGen counts the number of completed preemption// signals. This is used to detect when a preemption is// requested, but fails.preemptGen atomic.Uint32// Whether this is a pending preemption signal on this M.signalPending atomic.Uint32// pcvalue lookup cachepcvalueCache pcvalueCachedlogPerMmOSchacha8   chacha8rand.Statecheaprand uint64// Up to 10 locks held by this m, maintained by the lock ranking code.locksHeldLen intlocksHeld    [10]heldLockInfo
}

主要的有:

  • g0: Go 运行时系统在启动之初创建的,用来调度其他 G 到 M 上;
  • mstartfn: 表示M的起始函数,go 语句携带的那个函数;
  • ​ curg: m 启动时调用的函数,通常是 runtime.startTheWorld 或类似的启动函数,负责初始化新线程(M)并开始执行 Go 代码。
  • p: 表示当前 m 绑定的处理器(P)。如果 m 正在执行 Go 代码,这个字段将指向一个有效的 p;否则,如果 m 正在执行系统调用或非 Go 代码,这个字段可能为 nil。
  • nextp: 这个字段用于在系统调用期间临时存储 p。当 m 由于系统调用而阻塞时,它的 p 可能会被传递给另一个 m,此时 nextp 会存储这个 p。
  • spinning: 表示当前 M 是否正在寻找 G,在寻找过程中 M 处于自旋状态;
  • lockedg: 这个字段指向一个与当前 m 锁定的 goroutine。当一个 goroutine 通过运行时系统调用(如锁的获取)与 m 锁定时,lockedg 会指向这个 goroutine。锁定期间,其他 goroutine 无法在该 m 上执行。

跟G一样,m也有另外两类:

  • m0: Go程序是一个进程,进程都有一个主线程,m0就是Go程序的主线程,通过一个与其绑定的G0来执行runtime启动加载代码;一个Go程序只有一个m0
  • 运行sysmon的M: 主要用来运行sysmon协程。定义在全局变量runtime.m0中,不需要在heap(堆)上分配;
    负责执行初始化操作和启动第一个g( g0);

例如在proc.go文件中就是:

var (m0           mg0           gmcache0      *mcacheraceprocctx0 uintptrraceFiniLock mutex
)

在这里中m0、g0是全局的。但是虽然这里看来g0是全局一个,但是这里的g0并不是抽象的g0,这里的g0仅代表和m0绑定的g0。 我们从创建m的代码看起:


// Create a new m. It will start off with a call to fn, or else the scheduler.
// fn needs to be static and not a heap allocated closure.
// May run with m.p==nil, so write barriers are not allowed.
//
// id is optional pre-allocated m ID. Omit by passing -1.
//
//go:nowritebarrierrec
func newm(fn func(), pp *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()// 这里实现m的创建mp := allocm(pp, fn, id)mp.nextp.set(pp)mp.sigmask = initSigmaskif gp := getg(); gp != nil && gp.m != nil && (gp.m.lockedExt != 0 || gp.m.incgo) && GOOS != "plan9" {// We're on a locked M or a thread that may have been// started by C. The kernel state of this thread may// be strange (the user may have locked it for that// purpose). We don't want to clone that into another// thread. Instead, ask a known-good thread to create// the thread for us.//// This is disabled on Plan 9. See golang.org/issue/22227.//// TODO: This may be unnecessary on Windows, which// doesn't model thread creation off fork.lock(&newmHandoff.lock)if newmHandoff.haveTemplateThread == 0 {throw("on a locked thread with no template thread")}mp.schedlink = newmHandoff.newmnewmHandoff.newm.set(mp)if newmHandoff.waiting {newmHandoff.waiting = falsenotewakeup(&newmHandoff.wake)}unlock(&newmHandoff.lock)// The M has not started yet, but the template thread does not// participate in STW, so it will always process queued Ms and// it is safe to releasem.releasem(getg().m)return}// 这里分配真正的操作系统newm1(mp)releasem(getg().m)
}

然后看真正的创建m的func:

func allocm(pp *p, fn func(), id int64) *m {// 通过allocmLock.rlock() 获取 allocmLock 的读锁,以确保在创建 M 的过程中不会被其他线程中断。allocmLock.rlock()// The caller owns pp, but we may borrow (i.e., acquirep) it. We must// disable preemption to ensure it is not stolen, which would make the// caller lose ownership.// 通过调用 acquirem() 禁用抢占,以确保当前的 P(处理器)不会被窃取。这是因为在接下来的操作中,可能会临时借用 P 进行内存分配。acquirem()gp := getg()// 如果当前的 G 所属的 M 没有关联的 P,则通过调用 acquirep(pp) 临时借用 P 进行内存分配。if gp.m.p == 0 {acquirep(pp) // temporarily borrow p for mallocs in this function}// Release the free M list. We need to do this somewhere and// this may free up a stack we can use.// 释放空闲 M 列表。这一步可能会释放可用的栈资源。if sched.freem != nil {lock(&sched.lock)var newList *mfor freem := sched.freem; freem != nil; {// Wait for freeWait to indicate that freem's stack is unused.wait := freem.freeWait.Load()if wait == freeMWait {next := freem.freelinkfreem.freelink = newListnewList = freemfreem = nextcontinue}// Drop any remaining trace resources.// Ms can continue to emit events all the way until wait != freeMWait,// so it's only safe to call traceThreadDestroy at this point.if traceEnabled() || traceShuttingDown() {traceThreadDestroy(freem)}// Free the stack if needed. For freeMRef, there is// nothing to do except drop freem from the sched.freem// list.if wait == freeMStack {// stackfree must be on the system stack, but allocm is// reachable off the system stack transitively from// startm.systemstack(func() {stackfree(freem.g0.stack)})}freem = freem.freelink}sched.freem = newListunlock(&sched.lock)}mp := new(m)mp.mstartfn = fnmcommoninit(mp, id)// In case of cgo or Solaris or illumos or Darwin, pthread_create will make us a stack.// Windows and Plan 9 will layout sched stack on OS stack.// 根据操作系统的不同,为新的 M 创建一个初始的 G0(用于执行该 M 上的 goroutines)。如果是 cgo、Solaris、illumos 或 Darwin 系统,会通过 malg(-1) 创建一个栈;否则会通过 malg(16384 * sys.StackGuardMultiplier) 创建一个指定大小的栈。if iscgo || mStackIsSystemAllocated() {mp.g0 = malg(-1)} else {mp.g0 = malg(16384 * sys.StackGuardMultiplier)}mp.g0.m = mp// 如果之前临时借用了 P,则通过 releasep() 归还 Pif pp == gp.m.p.ptr() {releasep()}// 释放当前 G 所属的 M,并释放 allocmLock 的读锁releasem(gp.m)allocmLock.runlock()return mp
}

可以看出在如下代码:

if iscgo || mStackIsSystemAllocated() {mp.g0 = malg(-1)} else {mp.g0 = malg(16384 * sys.StackGuardMultiplier)}

会根据创建操作系统不同创建不同的g0。所以总结m0只有一个,但是g0有很多个,直接与m0绑定,它的内存是操作系统提供的虚拟内存空间中分配的 。当然这从结构体上也可以看出来,m结构体设计中有一个单独的g0。这里也细看了下mac os在源码中分配g0的stack内存大小为:16384byte,也就是16kb

从m的结构体的spinning也可以看出来m主要就两个状态自旋中,与非自旋中:

  • 自旋状态: 表示 M 绑定了 P 又没有获取 G;
  • 非自旋状态: 表示正在执行 Go 代码中,或正在进入系统调用,或空闲;

p结构体:

type p struct {// 全局变量allp中的索引位置id int32// p的状态标识status uint32 // one of pidle/prunning/...link   puintptr// 用于sysmon协程记录记录被监控的p的系统调用时间和运行时间schedtick   uint32     // incremented on every scheduler callsyscalltick uint32     // incremented on every system callsysmontick  sysmontick // last tick observed by sysmon// 指向绑定的m,idle状态下为nilm muintptr // back-link to associated m (nil if idle)// 用于分配微小对象和小对象的一个块的缓存空间,含有各种不同等级的spanmcache *mcache// 一个chunk大小(512kb)的内存空间,用来对堆上内存分配的缓存优化,达到无锁缓存pcache      pageCacheraceprocctx uintptrdeferpool    []*_defer // pool of available defer structs (see panic.go)deferpoolbuf [32]*_defer// Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.// 可以分配给g的id的缓存goidcache    uint64goidcacheend uint64// 本地可运行的G队列的头部和尾部// Queue of runnable goroutines. Accessed without lock.runqhead uint32runqtail uint32runq     [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.//// Note that while other P's may atomically CAS this to zero,// only the owner P can CAS it to a valid G.// 下一个待运行的g,这个g的优先级最高// 如果当前g运行完后还有剩余可用时间,那么就会运行runnext的grunnext guintptr// Available G's (status == Gdead)// p上的空闲队列列表gFree struct {gListn int32}sudogcache []*sudogsudogbuf   [128]*sudog// Cache of mspan objects from the heap.mspancache struct {// We need an explicit length here because this field is used// in allocation codepaths where write barriers are not allowed,// and eliminating the write barrier/keeping it eliminated from// slice updates is tricky, more so than just managing the length// ourselves.len intbuf [128]*mspan}// Cache of a single pinner object to reduce allocations from repeated// pinner creation.pinnerCache *pinnertrace pTraceStatepalloc persistentAlloc // per-P to avoid mutex// Per-P GC stategcAssistTime         int64 // Nanoseconds in assistAllocgcFractionalMarkTime int64 // Nanoseconds in fractional mark worker (atomic)// limiterEvent tracks events for the GC CPU limiter.limiterEvent limiterEvent// gcMarkWorkerMode is the mode for the next mark worker to run in.// That is, this is used to communicate with the worker goroutine// selected for immediate execution by// gcController.findRunnableGCWorker. When scheduling other goroutines,// this field must be set to gcMarkWorkerNotWorker.gcMarkWorkerMode gcMarkWorkerMode// gcMarkWorkerStartTime is the nanotime() at which the most recent// mark worker started.gcMarkWorkerStartTime int64// gcw is this P's GC work buffer cache. The work buffer is// filled by write barriers, drained by mutator assists, and// disposed on certain GC state transitions.gcw gcWork// wbBuf is this P's GC write barrier buffer.//// TODO: Consider caching this in the running G.wbBuf wbBufrunSafePointFn uint32 // if 1, run sched.safePointFn at next safe point// statsSeq is a counter indicating whether this P is currently// writing any stats. Its value is even when not, odd when it is.statsSeq atomic.Uint32// Timer heap.timers timers// maxStackScanDelta accumulates the amount of stack space held by// live goroutines (i.e. those eligible for stack scanning).// Flushed to gcController.maxStackScan once maxStackScanSlack// or -maxStackScanSlack is reached.maxStackScanDelta int64// gc-time statistics about current goroutines// Note that this differs from maxStackScan in that this// accumulates the actual stack observed to be used at GC time (hi - sp),// not an instantaneous measure of the total stack size that might need// to be scanned (hi - lo).// 当前goroutines的gc时间统计信息scannedStackSize uint64 // stack size of goroutines scanned by this PscannedStacks    uint64 // number of goroutines scanned by this P// preempt is set to indicate that this P should be enter the// scheduler ASAP (regardless of what G is running on it).// 判断是否被抢占preempt bool// gcStopTime is the nanotime timestamp that this P last entered _Pgcstop.gcStopTime int64// pageTraceBuf is a buffer for writing out page allocation/free/scavenge traces.//// Used only if GOEXPERIMENT=pagetrace.pageTraceBuf pageTraceBuf// Padding is no longer needed. False sharing is now not a worry because p is large enough// that its size class is an integer multiple of the cache line size (for any of our architectures).
}
  • p的状态

_Pidle: 表示 P 目前没有执行任何用户代码,也没有参与调度工作。它的本地运行队列是空的,没有 goroutine 可以运行。P 在这种状态下等待新的任务或者返回到空闲 P 的集合中。
_Prunning: 表示P 的正常运行状态,表示它当前绑定到一个线程(M),并且正在执行用户代码或者调度器的代码。在这种状态下,P 会积极地从它的本地运行队列中取出 goroutine 并执行它们。
_Psyscall: 当 P 绑定的 M 执行系统调用时,P 会进入这个状态。这意味着尽管 M 正在执行代码,但它并没有执行 Go 语言层面的用户代码,而是在执行操作系统层面的调用(syscall)。
_Pgcstop: 此状态与垃圾回收(GC)相关。当垃圾回收需要协助或者 P 需要在垃圾回收期间暂停时,P 会进入这个状态。在 _Pgcstop 状态下,P 不会执行任何用户代码,直到垃圾回收完成。
_Pdead: 这个状态表示 P 已经不再被调度器使用,并且已经被回收或者移除了。在程序的生命周期中,P 的数量可能会动态变化,当 P 不再需要时,它们会被标记为 _Pdead 并最终被销毁。

它们之间的关系为如下图(来源腾讯技术工程公众号​):
在这里插入图片描述


  • P的创建
    P的初始化是在schedinit函数中调用的,schedinit函数是在runtime的汇编启动代码里调用的。
func schedinit() {// 初始化各种锁lockInit(&sched.lock, lockRankSched)lockInit(&sched.sysmonlock, lockRankSysmon)lockInit(&sched.deferlock, lockRankDefer)lockInit(&sched.sudoglock, lockRankSudog)lockInit(&deadlock, lockRankDeadlock)lockInit(&paniclk, lockRankPanic)lockInit(&allglock, lockRankAllg)lockInit(&allpLock, lockRankAllp)lockInit(&reflectOffs.lock, lockRankReflectOffs)lockInit(&finlock, lockRankFin)lockInit(&cpuprof.lock, lockRankCpuprof)allocmLock.init(lockRankAllocmR, lockRankAllocmRInternal, lockRankAllocmW)execLock.init(lockRankExecR, lockRankExecRInternal, lockRankExecW)traceLockInit()// Enforce that this lock is always a leaf lock.// All of this lock's critical sections should be// extremely short.lockInit(&memstats.heapStats.noPLock, lockRankLeafRank)// raceinit must be the first call to race detector.// In particular, it must be done before mallocinit below calls racemapshadow.gp := getg()if raceenabled {gp.racectx, raceprocctx0 = raceinit()}sched.maxmcount = 10000crashFD.Store(^uintptr(0))// The world starts stopped.worldStopped()ticks.init() // run as early as possiblemoduledataverify()stackinit()mallocinit()godebug := getGodebugEarly()initPageTrace(godebug) // must run after mallocinit but before anything allocatescpuinit(godebug)       // must run before alginitrandinit()             // must run before alginit, mcommoninitalginit()              // maps, hash, rand must not be used before this callmcommoninit(gp.m, -1)modulesinit()   // provides activeModulestypelinksinit() // uses maps, activeModulesitabsinit()     // uses activeModulesstkobjinit()    // must run before GC startssigsave(&gp.m.sigmask)initSigmask = gp.m.sigmaskgoargs()goenvs()secure()checkfds()parsedebugvars()gcinit()// Allocate stack space that can be used when crashing due to bad stack// conditions, e.g. morestack on g0.gcrash.stack = stackalloc(16384)gcrash.stackguard0 = gcrash.stack.lo + 1000gcrash.stackguard1 = gcrash.stack.lo + 1000// if disableMemoryProfiling is set, update MemProfileRate to 0 to turn off memprofile.// Note: parsedebugvars may update MemProfileRate, but when disableMemoryProfiling is// set to true by the linker, it means that nothing is consuming the profile, it is// safe to set MemProfileRate to 0.if disableMemoryProfiling {MemProfileRate = 0}lock(&sched.lock)sched.lastpoll.Store(nanotime())procs := ncpuif n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {procs = n}if procresize(procs) != nil {throw("unknown runnable goroutine during bootstrap")}unlock(&sched.lock)// World is effectively started now, as P's can run.worldStarted()if buildVersion == "" {// Condition should never trigger. This code just serves// to ensure runtime·buildVersion is kept in the resulting binary.buildVersion = "unknown"}if len(modinfo) == 1 {// Condition should never trigger. This code just serves// to ensure runtime·modinfo is kept in the resulting binary.modinfo = ""}
}

可以看出具体创建的调度器的个数取决于GOMAXPROCS参数,而如果没有默认的则取决于当初cpu的核数。

procs := ncpuif n, ok := atoi32(gogetenv("GOMAXPROCS")); ok && n > 0 {procs = n}if procresize(procs) != nil {throw("unknown runnable goroutine during bootstrap")}

一般来讲,程序运行时就将GOMAXPROCS大小设置为CPU核数,可让Go程序充分利用CPU。在某些IO密集型的应用
里,这个值可能并不意味着性能最好。
理论上当某个Goroutine进入系统调用时,会有一个新的M被启用或创建,继
续占满CPU。但由于Go调度器检测到M被阻塞是有一定延迟的,也即旧的M被阻塞和新的M得到运行之间是有一定间隔
的,所以在IO密集型应用中不妨把GOMAXPROCS设置的大一些,或许会有好的效果。

然后具体初始化,以及后续p的调整都在对应的procresize这个函数里:

// Change number of processors.
//
// sched.lock must be held, and the world must be stopped.
//
// gcworkbufs must not be being modified by either the GC or the write barrier
// code, so the GC must not be running if the number of Ps actually changes.
//
// Returns list of Ps with local work, they need to be scheduled by the caller.
func procresize(nprocs int32) *p {// 为了确保在调整处理器数量时系统调度是安全的assertLockHeld(&sched.lock)assertWorldStopped()// 验证arg 合法性old := gomaxprocsif old < 0 || nprocs <= 0 {throw("procresize: invalid arg")}// 如果开启了trace则记录当前Gomaxprocs的变化trace := traceAcquire()if trace.ok() {trace.Gomaxprocs(nprocs)traceRelease(trace)}// 更新统计数据(处理器数量调整的时间等)now := nanotime()if sched.procresizetime != 0 {sched.totaltime += int64(old) * (now - sched.procresizetime)}sched.procresizetime = nowmaskWords := (nprocs + 31) / 32// 根据新的处理器数量调整全局 allp 数组的大小,这个数组保存了所有处理器(P)的引用if nprocs > int32(len(allp)) {// Synchronize with retake, which could be running// concurrently since it doesn't run on a P.lock(&allpLock)if nprocs <= int32(cap(allp)) {allp = allp[:nprocs]} else {nallp := make([]*p, nprocs)// Copy everything up to allp's cap so we// never lose old allocated Ps.copy(nallp, allp[:cap(allp)])allp = nallp}if maskWords <= int32(cap(idlepMask)) {idlepMask = idlepMask[:maskWords]timerpMask = timerpMask[:maskWords]} else {nidlepMask := make([]uint32, maskWords)// No need to copy beyond len, old Ps are irrelevant.copy(nidlepMask, idlepMask)idlepMask = nidlepMaskntimerpMask := make([]uint32, maskWords)copy(ntimerpMask, timerpMask)timerpMask = ntimerpMask}unlock(&allpLock)}// 初始化pfor i := old; i < nprocs; i++ {pp := allp[i]if pp == nil {pp = new(p)}pp.init(i)atomicstorep(unsafe.Pointer(&allp[i]), unsafe.Pointer(pp))}// check当前的goroutine是否需要绑定新的p,还是使用旧的gp := getg()if gp.m.p != 0 && gp.m.p.ptr().id < nprocs {// continue to use the current Pgp.m.p.ptr().status = _Prunninggp.m.p.ptr().mcache.prepareForSweep()} else {// release the current P and acquire allp[0].//// We must do this before destroying our current P// because p.destroy itself has write barriers, so we// need to do that from a valid P.if gp.m.p != 0 {trace := traceAcquire()if trace.ok() {// Pretend that we were descheduled// and then scheduled again to keep// the trace consistent.trace.GoSched()trace.ProcStop(gp.m.p.ptr())traceRelease(trace)}gp.m.p.ptr().m = 0}gp.m.p = 0pp := allp[0]pp.m = 0pp.status = _Pidleacquirep(pp)trace := traceAcquire()if trace.ok() {trace.GoStart()traceRelease(trace)}}// g.m.p is now set, so we no longer need mcache0 for bootstrapping.mcache0 = nil// 释放不在试用的pfor i := nprocs; i < old; i++ {pp := allp[i]pp.destroy()// can't free P itself because it can be referenced by an M in syscall}// 调整 allp 数组大小,如果 allp 数组的大小大于新的处理器数量,将其截断到合适的大小。if int32(len(allp)) != nprocs {lock(&allpLock)allp = allp[:nprocs]idlepMask = idlepMask[:maskWords]timerpMask = timerpMask[:maskWords]unlock(&allpLock)}// 构建一个链表,包含所有处于空闲状态但有本地工作队列的 P,这些 P 需要由调用者调度。var runnablePs *pfor i := nprocs - 1; i >= 0; i-- {pp := allp[i]if gp.m.p.ptr() == pp {continue}pp.status = _Pidleif runqempty(pp) {pidleput(pp, now)} else {pp.m.set(mget())pp.link.set(runnablePs)runnablePs = pp}}stealOrder.reset(uint32(nprocs))var int32p *int32 = &gomaxprocs // make compiler check that gomaxprocs is an int32atomic.Store((*uint32)(unsafe.Pointer(int32p)), uint32(nprocs))if old != nprocs {// Notify the limiter that the amount of procs has changed.gcCPULimiter.resetCapacity(now, nprocs)}return runnablePs
}

然后p会在pp.init(i)进一步的初始化:

// init initializes pp, which may be a freshly allocated p or a
// previously destroyed p, and transitions it to status _Pgcstop.
func (pp *p) init(id int32) {pp.id = idpp.status = _Pgcstop               // // 表示这个处理器目前处于垃圾收集停止状态pp.sudogcache = pp.sudogbuf[:0]    // 存储等待运行的 goroutine(称为 sudog)的缓存。pp.deferpool = pp.deferpoolbuf[:0] // 存储延迟函数调用(defer)的池pp.wbBuf.reset()                   // 用于跟踪在并发执行期间对内存的写入操作/**为 p 分配或关联一个 mcache(内存缓存),mcache 是用来缓存分配的内存块以加速内存分配过程的。如果 id 为 0,使用一个特殊的 mcache 实例 mcache0,这是启动时用于引导的内存缓存。否则,为这个 p 分配一个新的 mcache。*/if pp.mcache == nil {if id == 0 {if mcache0 == nil {throw("missing mcache?")}// Use the bootstrap mcache0. Only one P will get// mcache0: the one with ID 0.pp.mcache = mcache0} else {pp.mcache = allocmcache()}}// 关联race上下文if raceenabled && pp.raceprocctx == 0 {if id == 0 {pp.raceprocctx = raceprocctx0raceprocctx0 = 0 // bootstrap} else {pp.raceprocctx = raceproccreate()}}lockInit(&pp.timers.mu, lockRankTimers)// This P may get timers when it starts running. Set the mask here// since the P may not go through pidleget (notably P 0 on startup).timerpMask.set(id)// Similarly, we may not go through pidleget before this P starts// running if it is P 0 on startup.idlepMask.clear(id)
}

所以从代码层面可以再总结下:

  • g (goroutine): 表示一个 Go 协程,是 Go 并发的执行单位。每个 g 代表一个可运行的程序计数器(pc)和栈,它包含了执行 Go 代码所需的状态。g 可以被阻塞(如等待 channel 操作)、运行或处于其他状态。

  • m (machine): 表示一个操作系统线程,是实际执行 g 的载体。每个 m 都有一个 g0 协程,这个 g0 用于调度,也就是说,当 m 执行调度操作时(如创建新的 g 或者进行垃圾回收),它使用的是 g0。m 负责执行 g 的代码,并且管理与操作系统线程相关的操作。

  • p (processor): 是 Go 运行时中的逻辑处理器,它作为 m 和 g 之间的桥梁。每个 p 都有一个运行队列(run queue),包含多个 g,m 可以执行绑定在 p 上的 g。p 还管理着内存分配,每个 p 都有一个本地的 mcache,用于优化内存分配。

三者关系如下:

  • m 与 g 的关系: 一个 m 可以执行多个 g,但它在任一时刻只能执行一个 g。m 通过其 curg 字段跟踪当前正在执行的 g。
  • m 与 p 的关系: 一个 m 可以绑定到一个 p 上,以执行该 p 上的 g。m 通过 p 字段来引用当前绑定的 p。
  • p 与 g 的关系: 一个 p 包含一个运行队列,其中包含多个 g。p 负责调度这些 g 的执行,并且通过 runq(一个固定大小的数组)来管理它们。

P与M创建的实际

  • 创建 P(Processor):

    • 程序启动时: 在 Go 程序启动时,运行时系统会根据 GOMAXPROCS 环境变量的值或通过 runtime.GOMAXPROCS 函数设置的值来创建相应数量的 P。GOMAXPROCS 决定了程序运行时最多可以使用的处理器数量,从而影响并行执行的 goroutine 的数量。没有参数则通过cpu数量设定默认的值。

    • 运行时调整: 虽然 P 的数量通常在程序启动时确定,但可以通过改变 runtime.GOMAXPROCS 的值来动态调整。这会导致运行时系统根据新的 GOMAXPROCS 值增加或减少 P 的数量。

  • 创建 M (Machine, 即 OS Thread):

    • 需要执行 Go 代码时: 当系统中没有足够的 M 来关联 P 并执行其中的可运行的 G(goroutine)时,会创建新的 M。例如,当所有的 M 都阻塞在系统调用上,而 P 中还有许多可运行的 G 时,系统会寻找空闲的 M,如果没有找到,就会创建新的 M。

    • M 阻塞时: 如果一个 M 在执行系统调用时阻塞,它可能不会立即创建新的 M,而是尝试将当前 P 转移给其他空闲的 M。如果找不到空闲的 M,则会创建新的 M。

    • 初始化和辅助 M: 程序启动时会创建一个特殊的 M(通常称为 M0),用于执行程序的初始化和启动工作。此外,还有一些辅助 M,如运行时系统内部的系统监控器(sysmon)和垃圾回收标记辅助器(gcMarkWorker),它们在需要时会被创建。

    • 垃圾回收: 在执行垃圾回收时,可能需要额外的 M 来帮助完成标记和清扫工作。

2.4 调度流程

从总的调度流程上可以参考刘丹冰老师的《Golang的协程调度器原理及GMP设计思想》的图:

在这里插入图片描述

  • 从图中可以得出开始一个go func的流程为:
  1. 通过 go func()来创建⼀个goroutine;
  2. 有两个存储G的队列,⼀个是局部调度器P的本地队列、⼀个是全局G队列。新创建的G会先
    保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中;
  3. G只能运⾏在M中,⼀个M必须持有⼀个P,M与P是1:1的关系。M会从P的本地队列弹出⼀个可执
    ⾏状态的G来执⾏,如果P的本地队列为空,就会想其他的MP组合偷取⼀个可执⾏的G来执⾏;
  4. ⼀个M调度G执⾏的过程是⼀个循环机制;
  5. 当M执⾏某⼀个G时候如果发⽣了syscall或则其余阻塞操作,M会阻塞,如果当前有⼀些G在执⾏,
    runtime会把这个线程M从P中摘除(detach),然后再创建⼀个新的操作系统的线程(如果有空闲的线程可
    ⽤就复⽤空闲线程)来服务于这个P;
  6. 当M系统调⽤结束时候,这个G会尝试获取⼀个空闲的P执⾏,并放⼊到这个P的本地队列。如果获取不
    到P,那么这个线程M变成休眠状态, 加⼊到空闲线程中,然后这个G会被放⼊全局队列中。
  • 调度的生命周期:
    在这里插入图片描述

2.5 设计思想

复用线程: 避免频繁的创建、销毁线程,减少这部分的开销。

  • work stealing机制:Go 调度器中的 work-stealing(工作窃取)机制是一种用于提高并发性能的策略,它允许一个处理器(P)在没有可运行的 goroutine 时,从其他处理器的本地队列中窃取(steal)一些 goroutine 来执行。这种机制有助于实现负载均衡,避免某些处理器过载而其他处理器空闲的情况。 主要体现在调度器的 runqget 和 runqsteal 函数中。
// 从本地可运行队列中获取 goroutine。
// 如果 inheritTime 为 true,则 gp 应继承当前时间片的剩余时间。
// 否则,它应该开始一个新的时间片。
// 仅由当前 P 的所有者执行。
func runqget(pp *p) (gp *g, inheritTime bool) {// 如果存在 runnext,则它是下一个要运行的 G。next := pp.runnext// 如果 runnext 非零并且 CAS 失败,那么它只能是被另一个 P 偷走了。if next != 0 && pp.runnext.cas(next, 0) {return next.ptr(), true // 返回 runnext G 并继承剩余时间片}for {h := atomic.LoadAcq(&pp.runqhead) // 原子加载头部索引t := pp.runqtail// 如果头部和尾部相等,队列为空if t == h {return nil, false}// 从本地队列头部取出 Ggp := pp.runq[h%uint32(len(pp.runq))].ptr()// 尝试原子地将头部索引向后移动if atomic.CasRel(&pp.runqhead, h, h+1) {return gp, false // 返回取出的 G 并开始新的时间片}}
}
// 从 p2 的本地可运行队列中偷取一半的 goroutine,
// 并将它们放入 p 的本地可运行队列。
// 返回偷取的其中一个 goroutine(如果失败则返回 nil)。
func runqsteal(pp, p2 *p, stealRunNextG bool) *g {t := pp.runqtail// 从 p2 偷取 goroutine 并放入 pp 的本地队列n := runqgrab(p2, &pp.runq, t, stealRunNextG)if n == 0 {return nil}n--// 获取偷取的 goroutinegp := pp.runq[(t+n)%uint32(len(pp.runq))].ptr()if n == 0 {return gp}// 原子加载 p 的本地队列头部索引h := atomic.LoadAcq(&pp.runqhead)// 检查是否溢出if t-h+n >= uint32(len(pp.runq)) {throw("runqsteal: runq overflow")}// 原子存储新的尾部索引,使偷取的 goroutine 可供消费atomic.StoreRel(&pp.runqtail, t+n)return gp
}// runqgrab 从给定处理器 pp 的本地可运行队列中批量获取(或“窃取”)goroutine。
// batch 是一个环形缓冲区,起始于 batchHead。
// 返回获取到的 goroutine 数量。
// 可以由任何 P 执行。
func runqgrab(pp *p, batch *[256]guintptr, batchHead uint32, stealRunNextG bool) uint32 {for {// 使用 load-acquire 操作加载 pp 的 runq 队列头部索引,与其他消费者同步。h := atomic.LoadAcq(&pp.runqhead)// 使用 load-acquire 操作加载 pp 的 runq 队列尾部索引,与生产者同步。t := atomic.LoadAcq(&pp.runqtail)// 计算可偷取的 goroutine 数量。n := t - h// 减半以确保不会取走所有 goroutine,留一半给原处理器。n = n - n/2// 如果没有可偷取的 goroutine 或者 stealRunNextG 为真且 pp.runnext 有值。if n == 0 {if stealRunNextG {// 尝试从 pp.runnext 偷取 goroutine。if next := pp.runnext; next != 0 {if pp.status == _Prunning {// 如果 pp 正在运行,则睡眠一小会,确保 pp 不会立即运行我们即将偷取的 goroutine。// 这种后退策略可以减少在不同 Ps 之间频繁交换 goroutine 的情况。if !osHasLowResTimer {usleep(3) // 如果系统不支持低分辨率定时器,则睡眠 3 微秒。} else {osyield() // 否则,进行线程让步(yield),放弃 CPU。}}// 使用 CAS 尝试将 pp.runnext 设置为 0,如果成功则表示偷取成功。if !pp.runnext.cas(next, 0) {continue}// 将偷取的 goroutine 放入 batch 缓冲区。batch[batchHead%uint32(len(batch))] = nextreturn 1 // 返回偷取的 goroutine 数量。}}return 0 // 如果没有可偷取的 goroutine,返回 0。}// 如果 h 和 t 读取不一致,说明队列状态可能已经改变,继续循环。if n > uint32(len(pp.runq)/2) {continue}// 将 pp 的 runq 队列中的 goroutine 复制到 batch 缓冲区。for i := uint32(0); i < n; i++ {g := pp.runq[(h+i)%uint32(len(pp.runq))]batch[(batchHead+i)%uint32(len(batch))] = g}// 使用 cas-release 操作尝试提交这次获取,如果成功则返回获取的 goroutine 数量。if atomic.CasRel(&pp.runqhead, h, h+n) {return n}}
}
  • Hand off(传递)机制是 Go 调度器中用于处理线程(M)阻塞时的一种策略。当一个线程因为系统调用或其他原因阻塞时,它不会让绑定的处理器(P)空闲,而是将当前的 P 传递给另一个线程,以便新线程可以继续执行 P 上的 goroutine。 这有助于减少因线程阻塞导致的上下文切换开销,并保持程序的并发性。

1. 线程阻塞: 线程 M 在执行系统调用或其他阻塞操作时被阻塞。
2. 寻找新线程: 调度器寻找一个新线程 M’,它可能是一个刚刚完成工作的线程,或者是新创建的线程。
3. 传递 P: 阻塞的线程 M 将其绑定的 P 传递给新线程 M’。
4. 新线程执行: 新线程 M’ 开始执行 P 上的 goroutine,而阻塞的线程 M 等待其阻塞操作完成。
5. 返回执行: 一旦阻塞操作完成,线程 M 可以重新获取一个 P 并继续执行 goroutine。


  • 利⽤并⾏ :GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运⾏。
  • 全局 G 队列:在新的调度器中依然有全局 G 队列,但功能已经被弱化了,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。
  • 抢占调度: 在coroutine中要等待⼀个协程主动让出CPU才执⾏下⼀个协程也就是协调调度,在Go中,抢占调度是一种确保公平性的机制,它允许长时间运行的 goroutine 被中断,从而给其他 goroutine 运行的机会。 在go1.14 版本引入了基于系统信号的异步抢占调度(在此之前也是协调),使得即使在没有函数调用的循环中,goroutine 也可以被抢占。这种机制是通过在运行时的每个函数的序言(function prologue)中插入检查代码来实现的,以便在适当的时候进行抢占。
// 信号处理函数,用于执行抢占
func doSigPreempt(gp *g, ctxt *sigctxt) {...if wantAsyncPreempt(gp) {if ok, newpc := isAsyncSafePoint(gp, ctxt.sigpc(), ctxt.sigsp(), ctxt.siglr()); ok {ctxt.pushCall(funcPC(asyncPreempt), newpc)}}...
}

具体的测试可以通过以下的例子然后通过trace追踪测试下,这里不再赘述。

package mainimport ("fmt""os""runtime""runtime/trace""sync"
)func main() {runtime.GOMAXPROCS(1)f, _ := os.Create("trace.output")defer f.Close()_ = trace.Start(f)defer trace.Stop()var wg sync.WaitGroupfor i := 0; i < 10; i++ {wg.Add(1)go func() {defer wg.Done()t := 0for i:=0;i<1e8;i++ {t+=1}fmt.Println("total:", t)}()}wg.Wait()
}

3.总结

  • 为了解决 Go 早期(v1.1前)多线程 M 对应多协程 G 调度器的全局锁、中心化状态带来的锁竞争导致的性能下降等问题,Go 开发者引入了处理器 P 结构,形成了当前经典的 GMP 调度模型;
  • Go的调度机制: 本质上是两级线程模型,为了运行时在用户态提供的多个函数组成的一种机制,目的是高效地调度 G 到 M上去执行。
  • G 在运行时中的状态可以简化成三种:等待中_Gwaiting、可运行_Grunnable、运行中_Grunning,运行期间大部分情况是在这三种状态间来回切换;
  • M 的状态可以简化为只有两种:自旋和非自旋; 自旋状态,表示 M 绑定了 P 又没有获取 G;非自旋状态,表示正在执行 Go 代码中,或正在进入系统调用,或空闲;
  • P的状态可以简化为:空闲中 _Pidle、运行中_Prunning、系统调用中 _Psyscall、GC中_Pgcstop、死亡​ _Pdead
  • Go调度的核心思想: 尽可能的确保线程的复用(work Stealing、Hand off机制)、给予协作的抢占机制、利用多核并行能力,限制同时运行(不包含阻塞)的 M 线程数 等于 CPU 的核心数目。
  • 调度器的启动逻辑是: 初始化 g0 和 m0,并将二者互相绑定, m0 是程序启动后的初始线程,g0 是 m0 线程的系统栈代表的 G 结构体,负责普通 G 在 M 上的调度切换 --> runtime.schedinit():负责M、P 的初始化过程,分别调用runtime.mcommoninit() 初始化 M 的全局队列allm 、调用 runtime.procresize() 初始化全局 P 队列 allp --> runtime.newproc():负责获取空闲的 G 或创建新的 G --> runtime.mstart() 启动调度循环;
  • 调度器的循环逻辑是: 运行函数 schedule() --> 通过 runtime.globrunqget() 从全局队列、通过 runtime.runqget() 从 P 本地队列、 runtime.findrunnable 从各个地方,获取一个可执行的 G --> 调用 runtime.execute() 执行 G --> 调用 runtime.gogo() 在汇编代码层面上真正执行G --> 调用 runtime.goexit0() 执行 G 的清理工作,重新将 G 加入 P 的空闲队列 --> 调用 runtime.schedule() 进入下一次调度循环。

相关参考:

  • 《Golang的协程调度器原理及GMP设计思想》
  • 《深入Go语言之旅》
  • 《goland专家编程》
  • 《Go语言高级编程》
  • https://strikefreedom.top/archives/high-performance-implementation-of-goroutine-pool#toc-head-10
  • https://zhuanlan.zhihu.com/p/586236582
  • https://www.cnblogs.com/dojo-lzz/p/16342622.html
    https://blog.leonard.wang/archives/golangprunnext%E6%98%AF%E4%BB%80%E4%B9%88
  • https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/GMP%E5%8E%9F%E7%90%86%E4%B8%8E%E8%B0%83%E5%BA%A6.html
  • https://blog.csdn.net/apple_56973763/article/details/127045808

相关文章:

一文解析golang中的协程与GMP模型

文章目录 前言1、线程实现模型1.1、用户级线程与内核级线程1.2、内核级线程模型1.3、用户级线程模型1.3、两级线程模型 2、GMP模型2.1、GMP模型概述2.1、GMP v1版本 - GM模型2.2、GMP v2版本 - GMP模型2.3、GMP相关源码2.4 调度流程2.5 设计思想 3.总结 前言 并发(并行&#x…...

深入浅出地探讨Python中字符串拼接的原理及效率优化方法

文章目录 1. 字符串的不可变性与内存模型2. 字符串拼接的底层实现3. 字符串拼接的效率问题及其优化3.1 使用 join() 方法3.2 字符串格式化3.3 实践中的选择和建议 Python中的字符串是不可变的数据类型&#xff0c;这意味着一旦一个字符串被创建&#xff0c;它的内容就无法被改变…...

C语言--基础面试真题

1、局部变量和静态变量的区别 普通局部变量和静态局部变量区别 存储位置&#xff1a; 普通局部变量存储在栈上 静态局部变量存储在静态存储区 生命周期&#xff1a; 当函数执行完毕时&#xff0c;普通局部变量会被销毁 静态局部变量的生命周期则是整个程序运行期间&#…...

C# 异步编程模型(APM)的深入理解

在现代软件开发中&#xff0c;异步编程已经成为了一种不可或缺的技能。特别是在需要处理I/O密集型任务或长时间运行的操作时&#xff0c;异步编程能够显著提高应用程序的性能和用户体验。C# 作为一种流行的编程语言&#xff0c;提供了多种异步编程模型&#xff0c;其中最经典的…...

janus模块介绍-SIP Gateway

模块启动 默认的SIP GateWay也是https协议&#xff0c;端口为8088或者8089 如果需要在自己搭建的测试服务上测试SIP GateWay模块&#xff0c;则也需要修改为wss 具体改动如下: 找到/opt/janus/share/janus/demos/siptest.js var server "wss://" window.location…...

wstunnel (websocket模式ssh)

接上一篇 修改客户端运行参数 ssh -o ProxyCommand"./wstunnel client -L stdio://%h:%p ws://192.168.254.131:8080" 127.0.0.1 其中127.0.0.1为服务端的本地ssh访问&#xff0c;可以修改为通过服务端访问其他设备的ssh服务。例如&#xff1a; ssh -o ProxyComma…...

linux文件相关命令

一、linux文件相关命令 1、cd cd 是 “change directory” 的缩写&#xff0c;用于改变当前工作目录。 cd &#xff1a;不带任何参数时&#xff0c;会切换到用户的主目录&#xff08;通常是 /home/用户名&#xff09;。 cd ~&#xff1a;也会切换到用户的主目录。 cd .&…...

锂电池寿命预测 | Matlab基于GRU门控循环单元的锂电池寿命预测

目录 预测效果基本介绍程序设计参考资料 预测效果 基本介绍 锂电池寿命预测 | Matlab基于GRU门控循环单元的锂电池寿命预测 Matlab基于GRU的锂电池剩余寿命预测 基于GRU的锂电池剩余寿命预测&#xff08;单变量&#xff09; 运行环境Matlab2020及以上 锂电池的剩余寿命预测是…...

通往AGI路上,DPU将如何构建生成式AI时代的坚实算力基石?

4月19日&#xff0c;在以“重构世界 奔赴未来”为主题的2024中国生成式AI大会上&#xff0c;中科驭数作为DPU新型算力基础设施代表&#xff0c;受邀出席了中国智算中心创新论坛&#xff0c;发表了题为《以网络为中心的AI算力底座构建之路》主题演讲&#xff0c;勾勒出在通往AGI…...

无人驾驶 自动驾驶汽车 环境感知 精准定位 决策与规划 控制与执行 高精地图与车联网V2X 深度神经网络学习 深度强化学习 Apollo

无人驾驶 百度apollo课程 1-5 百度apollo课程 6-8 七月在线 无人驾驶系列知识入门到提高 当今,自动驾驶技术已经成为整个汽车产业的最新发展方向。应用自动驾驶技术可以全面提升汽车驾驶的安全性、舒适性,满足更高层次的市场需求等。自动驾驶技术得益于人工智能技术的应用…...

【QEMU系统分析之启动篇(十九)】

系列文章目录 第十九章 QEMU系统仿真的加速器上电后设置分析 文章目录 系列文章目录第十九章 QEMU系统仿真的加速器上电后设置分析 前言一、QEMU是什么&#xff1f;二、QEMU系统仿真的启动分析1.系统仿真的初始化代码2.主循环数据初始化3. os_setup_post()Windows 系统 os_set…...

Jmeter redis连接测试

Jmeter连接redis获取数据&#xff0c;一直连不上报错。最后只能通过java代码连接测试&#xff0c;最后只能自己动手。 import redis.clients.jedis.*;import java.io.IOException; import java.util.HashSet; import java.util.Set;/*** 单机版的Jedis连接池的用法*/ public c…...

使用Uiautomotorviewer无法获取手机页面元素+解决办法

在进行 Android 应用程序开发或测试时&#xff0c;有时会遇到以下错误&#xff1a; Error while obtaining UI hierarchy XML file: com.android.ddmlib.SyncException这个错误可能会导致开发或测试过程中的一些困扰&#xff0c;但有一个简单的解决方法&#xff1a; 解决方法…...

深度相机(3D相机)

传统的RGB彩色相机称为2D相机&#xff0c; 只能得到2D的图像信息&#xff0c; 无法得到物体与相机的距离信息&#xff0c;也就是深度信息。 顾名思义&#xff0c; 深度相机除了获取2D信息&#xff0c;还能得到深度信息&#xff0c;也叫RGBD相机&#xff0c; 或3D相机。 顺便提…...

了解时间复杂度和空间复杂度

在学习数据结构前&#xff0c;我们需要了解时间复杂度和空间复杂度的概念&#xff0c;这能够帮助我们了解数据结构。 算法效率分为时间效率和空间效率 时间复杂度 一个算法的复杂度与其执行的次数成正比。算法中执行基础操作的次数&#xff0c;为算法的时间复杂度。 我们采…...

[笔试强训]day2

1.牛牛的快递 题目链接&#xff1a;牛牛的快递_牛客题霸_牛客网 思路&#xff1a;分小于1.0kg和大于1.0kg&#xff0c;其中大于1.0kg的要“向上取整” &#xff0c;eg&#xff1a;1.7->2&#xff0c;2.0->2。注意一个点&#xff1a;第二个输入的参数是字符&#xff0c;要…...

android脱壳第二发:grpc-dumpdex加修复

上一篇我写的dex脱壳&#xff0c;写到银行类型的app的dex修复问题&#xff0c;因为dex中被抽取出来的函数的code_item_off 的偏移所在的内存&#xff0c;不在dex文件范围内&#xff0c;所以需要进行一定的修复&#xff0c;然后就停止了。本来不打算接着搞得&#xff0c;但是写了…...

在 TypeScript 中declare module 关键字用法

在 TypeScript 中&#xff0c;declare module 关键字用于声明模块的类型信息&#xff0c;这种声明通常出现在声明文件&#xff08;通常是 .d.ts 文件&#xff09;中。这对于当你需要为现有的 JavaScript 库或模块提供类型信息时非常有用&#xff0c;尤其是对于没有提供自己的类…...

NLP step by step -- 了解Transformer

Transformer模型 Transformer相关历史 首先我们先看一下有关Transformer模型的发展历史&#xff0c;下面的图是基于Transformer架构的一些关键模型节点&#xff1a; 图片来源于Hugging Face 图片来源于Hugging Face Transformer 架构 于 2017 年 6 月推出。原本研究的重点是…...

【Leetcode】vector刷题

&#x1f525;个人主页&#xff1a;Quitecoder &#x1f525;专栏&#xff1a;Leetcode刷题 目录 1.只出现一次的数字2.杨辉三角3.删除有序数组中的重复项4.只出现一次的数字II5.只出现一次的数字III6.电话号码的字母组合 1.只出现一次的数字 题目链接&#xff1a;136.只出现一…...

【图解计算机网络】从浏览器地址输入到网页显示的整个过程

从浏览器地址输入到网页显示的整个过程 整体流程DHCPhttp协议报文组装DNSTCP协议封装与TCP三次握手IP协议封装与路由表MAC地址与ARP协议交换机路由器 整体流程 从往浏览器输入一个地址到网页的显示&#xff0c;要经过很长的一个流程&#xff0c;中间涉及到计算机网络的许多知识…...

liqo学习及安装,k8s,kubernetes多集群互联

先按照官方的教程在虚拟机安装学习 在开始以下教程之前&#xff0c;您应该确保您的系统上安装了以下软件&#xff1a; Docker&#xff0c;容器运行时。Kubectl&#xff0c;Kubernetes 的命令行工具。 curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.…...

五种主流数据库:集合运算

关系型数据库中的表与集合理论中的集合类似&#xff0c;表是由行&#xff08;记录&#xff09;组成的集合。因此&#xff0c;SQL 支持基于数据行的各种集合运算&#xff0c;包括并集运算&#xff08;Union&#xff09;、交集运算&#xff08;Intersect&#xff09;和差集运算&a…...

java-springmvc 01

springmvc也是在spring framework中的&#xff0c;不是一个单独的项目 MVC就是和Tomcat有关。 01.MVC启动的第一步&#xff0c;启动Tomcat&#xff08;这个和springboot的run方法启动Tomcat有关&#xff09; 02.SpringMVC中&#xff0c;最为核心的就是DispatcherServlet&…...

SecuPress Pro 专业级WordPress网站安全防护插件优化版

下载地址&#xff1a;SecuPress Pro 专业版.zip SecuPress Pro&#xff1a;专业的WordPress安全解决方案 如果您没有时间进行每周扫描&#xff0c;SecuPress Pro将是您的理想选择。SecuPress Pro提供了所有SecuPress Free的功能&#xff0c;同时还增加了一些高级选项&#xff…...

linux 查看nginx日志

在 Linux 系统中&#xff0c;查看 Nginx 日志通常涉及以下几个步骤&#xff1a; 确定日志文件位置&#xff1a;Nginx 的日志文件通常位于 /etc/nginx/logs 或 /var/log/nginx。具体位置取决于您在安装 Nginx 时的配置。 查看访问日志&#xff1a;Nginx 的访问日志默认命名为 a…...

iOS - 多线程-GCD-队列组

文章目录 iOS - 多线程-GCD-队列组1. 队列组1.1 基本使用步骤 iOS - 多线程-GCD-队列组 开发过程中&#xff0c;有时候想实现这样的效果 多个任务并发执行所有任务执行完成后&#xff0c;进行下一步处理&#xff08;比如回到主线程刷新UI&#xff09; 1. 队列组 可以使用GC…...

1052. 【NOIP2016备赛】方阵操作(square)

1052. 【NOIP2016备赛】方阵操作(square) (Input: square.in, Output: square.out) 时间限制: 1 s 空间限制: 256 MB 题目描述 小 Z 给你一个 n n 的方阵&#xff0c;要求你完成 Q 次操作&#xff1a; 1. 1 i j k&#xff0c;将 ai,j 修改为 k。 2. 2 i j&#xff0c;交…...

Python打怪升级(4)

在计算机领域常常有说"合法"和"非法"指的是:是否合理&#xff0c;是否有效&#xff0c;并不是指触犯了法律。 random.randint(begin,end) 详细讲解一下这个random是指模板&#xff0c;也就是别人写好的代码直接来用&#xff0c;在Python当中&#xff0c;…...

2024年【危险化学品生产单位安全生产管理人员】考试题库及危险化学品生产单位安全生产管理人员考试报名

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 危险化学品生产单位安全生产管理人员考试题库参考答案及危险化学品生产单位安全生产管理人员考试试题解析是安全生产模拟考试一点通题库老师及危险化学品生产单位安全生产管理人员操作证已考过的学员汇总&#xff0c;…...

动态IP与静态IP的区别,你选对了吗?

在互联网世界中&#xff0c;IP地址是每台设备在网络上的唯一标识。这些地址可以是动态的&#xff0c;也可以是静态的。对于非专业人士来说&#xff0c;理解这两者之间的区别可能会有些困难。本文旨在深入探讨动态IP和静态IP的主要差异&#xff0c;帮助读者根据自己的需求做出明…...

jasypt组件死锁bug案例分享

事故描述 1、上午9.55发布了一个Apollo动态配置参数&#xff1b; 2、片刻后&#xff0c;服务器接口开始出现大量的超时告警&#xff0c;似乎是某资源被耗尽不足分配&#xff1b; 3、正值业务请求高峰的上午十点&#xff08;平台上午10点会有一些活动会拉一波用户流量&#x…...

golang上传文件到ftp服务器

之前有个业务需要把文件上传到ftp服务器&#xff0c;写了一个上传ftp的功能 package ftpimport "context"type Client interface {// UploadFile 上传文件UploadFile(ctx context.Context, opt *UploadFileOpt) error }type UploadFileOpt struct {Data […...

数据治理和数据管理 傻傻分不清楚?

互联网时代&#xff0c;数据&#xff0c;这一无形资产&#xff0c;已成为现代企业的核心竞争力。如何高效地管理和利用数据&#xff0c;成为企业关注的焦点。在这个过程中&#xff0c;数据治理&#xff08;Data Governance&#xff09;和数据管理&#xff08;Data Management&a…...

linux磁盘原理

在linux系统中&#xff0c;对磁盘进行管理与windows系统类似&#xff0c;都要先分区&#xff0c;格式化&#xff0c;创建文件系统&#xff0c;挂载目录&#xff0c;数据写入...

react 使用WEB3.0控件开发包 V3.0接入海康威视摄像头

1、下载官方安装包&#xff1a; 2、安装官方插件 3、引入文件 在public/index 中引入监控依赖&#xff0c;这三个文件可以在下载的官方demo中找到 4、react 中使用 useEffect(() > { const ipInfo :[192.168.xxxx];//初始化摄像头const WebVideoCtrl window.WebVideoCtrl…...

数据可视化———Tableau

基本认识&#xff1a; 维度&#xff1a;定性—字符串文本&#xff0c;日期和日期时间等等 度量&#xff1a;定量—连续值&#xff0c;一般属于数值 数据类型&#xff1a; 数值 日期/日期时间 字符串 布尔值 地理值 运算符 算数运算符&#xff1a;加减乘除,%取余&#xff0c;…...

【黑马头条】-day12项目部署和发布-jenkins

文章目录 1 持续集成2 软件开发模式2.1 瀑布模式2.2 敏捷开发2.2.1 迭代开发2.2.2 增量开发 3 Jenkins3.1 Jenkins安装3.1.1 导入镜像3.1.2 配置3.1.3 初始化设置 3.2 插件安装3.3 服务器环境准备3.3.1 Docker安装配置3.3.2 Git安装配置3.3.3 Maven安装配置 3.4 Jenkins工具配置…...

学习操作系统路线

操作系统 简介 本课程为计算机专业学生量身定制&#xff0c;补足计算机操作系统相关知识&#xff0c;查漏补缺&#xff0c;也可用于考研复习。内容包括&#xff1a;操作统概述、进程管理、内存管理、文件管理、输入/输出管理等章节。内容力求精炼、重点突出、条理清晰、深入浅…...

uniapp微信小程序(商城项目)

最近&#xff0c;闲来无事&#xff0c;打算学一下uniapp小程序 于是在跟着某站上学着做了一个小程序&#xff0c;主要是为了学uniapp和vue。某站黑马优购 完成的功能主要有&#xff1a;首页、搜索、分类和购物车。 有人问了为什么没有登录、和添加订单呢&#xff1f;问的很好…...

Linux的自动化脚本:使用crul命令下载文件,实现断点续传

目录 一、要求 二、解决思路 &#xff08;一&#xff09;curl工具可以进行文件传输&#xff0c;可以实现手动断点续传 1、使用 --range 选项&#xff1a; 2. 使用 --continue-at 选项&#xff1a; &#xff08;二&#xff09;编写shell脚本调用curl命令&#xff0c;实现自…...

Golang | Leetcode Golang题解之第46题全排列

题目&#xff1a; 题解&#xff1a; func permute(nums []int) [][]int {var (n len(nums)dfs func(vals []int) // 已选择数 排列为vals 后续回溯继续选择 直至选完ans [][]int)dfs func(vals []int) {//边界if len(vals) n {ans append(ans, vals)}//转移 枚举选哪个f…...

MySQL数据表记录删操作

删除操作 作用删除表里的记录行&#xff08;都是整行整行的删除的&#xff09; 1.单表的删除 语法&#xff1a; delete from 表名 where 要删除的记录筛选条件; 案例&#xff1a;删除员工编号大于203的员工信息 delete from employees where employee_id>203; 2.多表…...

Python浅谈清朝秋海棠叶版图

1、清朝疆域概述&#xff1a; 清朝是我国最后一个封建王朝&#xff0c;其始于1616年建州女真部努尔哈赤建立后金&#xff0c;此后统一女真各部、东北地区。后又降服漠南蒙古&#xff0c;1644年入关打败农民起义军、灭南明&#xff0c;削三藩&#xff0c;复台湾。后又收外蒙&am…...

Linux之线程管理

目录 第1关&#xff1a;创建线程 任务描述 相关知识 使用pthread_create函数创建线程 编程要求 答案&#xff1a; 第2关&#xff1a;线程挂起 任务描述 相关知识 使用pthread_join挂起线程 编程要求 答案&#xff1a; 第3关&#xff1a;线程终止 任务描述 相关知识 使用pthread…...

.net反射(Reflection)

文章目录 一.概念&#xff1a;二.反射的作用&#xff1a;三.代码案例&#xff1a;四.运行结果&#xff1a; 一.概念&#xff1a; .NET 反射&#xff08;Reflection&#xff09;是指在运行时动态地检查、访问和修改程序集中的类型、成员和对象的能力。通过反射&#xff0c;你可…...

白平衡简介

文章目录 白平衡的概念白平衡的调节常见的白平衡模式 白平衡的概念 白平衡是指摄影、摄像和显示技术中的一项重要概念&#xff0c;用于调节图像中的白色或中性灰色的色彩&#xff0c;使其看起来在不同光源条件下都是准确的白色或灰色。白平衡的主要目的是确保图像的色彩准确性…...

centos7.9下安装SVN服务

一、安装subversion yum install -y subversion #安装svn mkdir -p /data/svnrepos/java #自定义svn仓库位置/data/svnrepos&#xff0c;自定义一个项目叫svn&#xff08;这里新建目录&#xff09; svnadmin create /data/svnrepos/java #创建一…...

iStat Menus for Mac:强大的系统监控工具

iStat Menus for Mac是一款功能强大的系统监控工具&#xff0c;专为Mac用户设计&#xff0c;旨在帮助用户全面了解电脑的运行状态&#xff0c;提高电脑的性能和稳定性。 iStat Menus for Mac v6.73 (1239)中文版下载 该软件可以实时监测CPU使用率、内存占用、网络速度、硬盘活动…...

NumPy 1.26 中文官方指南(四)

附加文件 术语表 原文&#xff1a;numpy.org/doc/1.26/glossary.html (n,) 括号中跟着逗号的数字表示一个具有一个元素的元组。尾随逗号将一个元素元组与括号n区分开。 -1 在维度入口中&#xff0c;指示 NumPy 选择长度&#xff0c;以保持数组元素总数不变。 >>> n…...

判断操作系统版本

操作系统版本号windows 1010.0windows Server 201610.0windows 8.16.3windows Server 2012 R26.3windows 86.2windows Server 20126.2windows 76.1windows Server 2008 R26.1windows Server 20086.0windows Vista6.0windows Server 2003 R25.2windows XP 64-bit Edition5.2wind…...

git 第一次安装设置用户名密码

git config --global user.name ljq git config --global user.email 15137659164qq.com创建公钥命令 输入后一直回车 ssh-keygen -t rsa下面这样代表成功 这里是公钥的 信息输入gitee 中 输入下面命令看是否和本机绑定成功 ssh -T gitgitee.com如何是这样&#xff0c;恭喜…...

如何配置Jupyter Lab以允许远程访问和设置密码保护

如何配置Jupyter Lab以允许远程访问和设置密码保护 当陪你的人要下车时&#xff0c;即使不舍&#xff0c;也该心存感激&#xff0c;然后挥手道别。——宫崎骏《千与千寻》 在数据科学和机器学习工作流中&#xff0c;Jupyter Lab是一个不可或缺的工具&#xff0c;但是默认情况下…...

免费实用在线AI工具集合

免费在线工具 https://orcc.online/ ![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/bbec1d4869814584af2f7d7a441710c5.png pdf在线免费转word文档 https://orcc.online/pdf 时间戳转换 https://orcc.online/timestamp Base64 编码解码 https://orcc.online/b…...

【开发工具】pythontutor——在线内存可视化工具

笔者在学习RISC-V时&#xff0c;希望找到一款可视化的内存工具&#xff0c;遗憾目前还未找到。发现了pythontutor这个网站&#xff0c;可以对C、python等多种语言进行内存可视化。结果似乎是x86架构的&#xff0c;符合小端存储。 贴一下网址&#xff0c;原准备依据开源版本进行…...

UE5介绍及安装

**虚幻引擎5&#xff08;Unreal Engine 5&#xff0c;简称UE5&#xff09;**是由Epic Games公司开发的一款游戏引擎&#xff0c;它提供了强大的游戏开发功能和工具&#xff0c;帮助游戏开发者更加高效、灵活地创建出高品质的游戏。UE5在虚幻4的基础上进行了大量的升级和改进&am…...

基于FPGA的AD7705芯片驱动设计VHDL代码Quartus仿真

名称&#xff1a; 软件&#xff1a;Quartus基于FPGA的AD7705芯片驱动设计VHDL代码Quartus仿真&#xff08;文末获取&#xff09; 语言&#xff1a;VHDL 代码功能&#xff1a; AD77025芯片控制及串口输出 1、使用FPGA控制AD77025芯片&#xff0c;使其输出AD值 2、将数据计…...

北邮22级信通院DSP:实验三(1):FFT变换、IFFT变换(附每步8点变换蝶形图)保姆级讲解+用C++程序实现复数域的FFT变换和IFFT变换

北邮22信通一枚~ 跟随课程进度更新北邮信通院DSP的笔记、代码和文章&#xff0c;欢迎关注~ 获取更多文章&#xff0c;请访问专栏&#xff1a; 北邮22级信通院DSP_青山入墨雨如画的博客-CSDN博客 目录 一、预备知识 1.1 FFT算法 1.2.1由DFT到FFT 1.2.2 基2时域抽选算法 …...

洛谷P10397题解

题目描述 给定一条 std::freopen 语句&#xff0c;输出其操作的文件名称。 形式化地&#xff0c;std::freopen 语句都应该恰好是 std::freopen("<title>","<mode>",<stream>);其中 <title> 为其操作的文件名称。其至少包含一个…...

XORM 框架的使用

1、xorm 1.1、xorm 简介 xorm 是一个简单而强大的Go语言ORM库. 通过它可以使数据库操作非常简便。 特性 支持 struct 和数据库表之间的灵活映射&#xff0c;并支持自动同步事务支持同时支持原始SQL语句和ORM操作的混合执行使用连写来简化调用支持使用ID, In, Where, Limit,…...

【Java EE】多线程(三)线程状态

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》 | 《数据结构与算法》 | 《C生万物》 |《MySQL探索之旅》 |《Web世界探险家》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更…...

爬虫Python库Requests

一、介绍 Requests 是一个强大的 Python 库&#xff0c;用于发送 HTTP 请求。它使得与 RESTful API 进行交互变得非常简单。Requests 可以通过 GET、POST、PUT、DELETE 等方法发送各种类型的请求&#xff0c;并且支持自定义 HTTP 头、请求参数、数据、cookies 等。 使用 Requ…...