「Golang进阶之路」GMP底层结构和select实现说明
写在前面#
你可以理解本文为一个附件文章,主要是附录上 Go 的 GMP 模型中关于 G,M,P 的底层结构和多路复用 select 源码分析的,你可能需要先阅读这篇文章「Golang 进阶之路」底层原理篇并且建议您先了解 GMP 模型的调度过程,对于阅读系列文章,你可以直接跳过本文。
G 的底层结构#
// 取 goroutine 的首字母,主要保存 goroutine 的一些状态信息以及 CPU 的一些寄存器的值,
// 例如 IP 寄存器,以便在轮到本 goroutine 执行时,CPU 知道要从哪一条指令处开始执行。
// 当 goroutine 被调离 CPU 时,调度器负责把 CPU 寄存器的值保存在 g 对象的成员变量之中。
// 当 goroutine 被调度起来运行时,调度器又负责把 g 对象的成员变量所保存的寄存器值恢复到 CPU 的寄存器。
type g struct {
// 堆栈参数,描述实际的堆栈内存,包括栈底和栈顶指针等
stack stack // offset known to runtime/cgo
// 用于栈的扩张和收缩检查,抢占标志,用于检测栈溢出情况
stackguard0 uintptr // offset known to liblink
stackguard1 uintptr // offset known to liblink
// 指向当前Goroutine的panic信息,用于处理异常
_panic *_panic // innermost panic - offset known to liblink
//指向当前Goroutine的延迟调用(defer)信息,用于处理延迟函数的执行
_defer *_defer // innermost defer
//指向执行该Goroutine的工作线程(M)的指针,用于Goroutine的调度和执行
m *m // current m; offset known to arm liblink
//Goroutine的运行状态和上下文信息。(下面有gobuf结构体各个字段的注释)
sched gobuf
//syscallsp用于在 Goroutine 的状态为 Gsyscall(系统调用)时,指定在垃圾回收(GC)期间使用的 syscallsp 值。
//在 Golang 的运行时系统中,当 Goroutine 进入系统调用时,它的状态会被设置为 Gsyscall。
//系统调用是指 Goroutine 请求底层操作系统提供的服务,例如文件 I/O、网络 I/O等。
//在进行垃圾回收期间,运行时系统需要执行一些特殊的操作,以确保垃圾回收器正常工作,并且不会意外地影响到系统调用过程。
//为了实现这一点,运行时系统在 g 结构体中维护了 syscallsp 字段。
//syscallsp 字段存储了一个地址值,表示在 GC 期间应使用的 syscallsp 值。
//该值通常是指向调用系统调用的堆栈帧的栈指针(stack pointer)。
//通过保存 syscallsp 值,垃圾回收器可以在 GC 过程中正确地恢复系统调用的状态。
syscallsp uintptr // if status==Gsyscall, syscallsp = sched.sp to use during gc
//用于在 Goroutine 的状态为 Gsyscall(系统调用)时,在垃圾回收(GC)期间使用的 syscallpc 值。
//当 Goroutine 进入系统调用时,它的状态会被设置为 Gsyscall,表示该 Goroutine 正在执行一个系统调用操作。
//跟上面的syscallsp字段类似,也是为了确保在进行垃圾回收期间,垃圾回收器的正常工作。
//syscallpc的值通常是指向调用系统调用的指令(instruction)的程序计数器(program counter)。
//也是为了垃圾回收器可以在 GC 过程中正确地恢复系统调用的执行状态。
syscallpc uintptr // if status==Gsyscall, syscallpc = sched.pc to use during gc
//用于在回溯(traceback)期间检查堆栈顶部的预期栈指针(stack pointer)。
//回溯是一种在程序出现错误或异常情况时,追踪并打印调用堆栈信息的过程。这有助于开发人员定位问题的来源和上下文。
//stktopsp 字段存储了一个地址值,表示预期的堆栈顶部的栈指针(stack pointer)。
//在进行回溯时,运行时系统会检查堆栈的顶部,以确保其栈指针与 stktopsp 字段中的预期值匹配。
//通过使用 stktopsp 字段,运行时系统可以在回溯期间验证堆栈的一致性,以确保栈指针的正确性。
//这有助于检测堆栈溢出、错误的调用帧和其他潜在的问题。
stktopsp uintptr // expected sp at top of stack, to check in traceback
// param 是一个通用指针参数字段,用于传递 参数的其他存储空间将很难找到的情况下传递值。
//目前有三种使用方式
// 参数字段:
// 1. 当一个通道操作唤醒一个被阻塞的 goroutine 时,它会将 param 设置为指向已完成阻塞操作的 sudog。
//2. 通过 gcAssistAlloc1 向其调用者发出信号,表明该 goroutine 已完成GC 循环。以任何其他方式都是不安全的,因为 goroutine 的堆栈可能会在GC期间发生了移动;
//3. 通过 debugCallWrap 向新的 goroutine 传递参数,因为在运行时分配闭包是被禁止的。
param unsafe.Pointer
//Goroutine的原子状态,用于表示Goroutine的状态,比如运行状态、等待状态、休眠状态等。
atomicstatus atomic.Uint32
//栈锁标志,用于在并发情况下对栈进行加锁
stackLock uint32 // sigprof/scang lock; TODO: fold in to atomicstatus
// Goroutine的唯一标识符,用于唯一标识每个Goroutine
goid uint64
//与调度器(scheduler)相关联的链表指针
//调度器可以通过遍历 G 结构体的 schedlink 链表来选择下一个要执行的 Goroutine,从而实现调度和切换的功能。
//通过使用 schedlink 字段,Golang 的运行时系统可以有效地管理和调度 Goroutine,实现并发执行和调度的功能。
schedlink guintptr
//表示 Goroutine(g 结构体)被阻塞的近似时间。
//当 Goroutine 被阻塞时,runtime 系统会记录当前的时间,并将其存储在 waitsince 字段中。
//通过使用 waitsince 字段,可以在调度器中追踪 Goroutine 的阻塞时间,用于调度和统计等用途。
waitsince int64 // approx time when the g become blocked
//表示 Goroutine(g 结构体)等待的原因。
//waitreason 字段仅在 Goroutine 的状态(status 字段)为 Gwaiting(等待)时才会被使用。
//它存储了 Goroutine 等待的具体原因,以便在调度器中进行跟踪和诊断。
//waitreason 是一个枚举类型(enum),定义了多种可能的等待原因,比如:
//waitReasonZero: 零值,表示无具体等待原因。
//waitReasonIOWait: Goroutine 正在等待 I/O 操作完成。
//waitReasonChannelSend: Goroutine 正在等待向通道发送数据的操作完成。
//waitReasonChannelReceive: Goroutine 正在等待从通道接收数据的操作完成。
//waitReasonSleep: Goroutine 正在等待休眠结束。
waitreason waitReason // if status==Gwaiting
//表示 Goroutine(g 结构体)是否收到了抢占信号。
//当 preempt 字段为 true 时,表示该 Goroutine 已经收到了抢占信号。
//抢占信号可以来自于其他更高优先级的 Goroutine 或运行时系统的调度器。
//抢占信号的目的是中断当前 Goroutine 的执行,以便调度器可以切换到其他 Goroutine。
//通过使用 preempt 字段,可以在合适的时机中断当前 Goroutine 的执行,以实现公平的 Goroutine 调度。
//这对于防止某个 Goroutine 长时间占用 CPU 资源,从而导致其他 Goroutine 饥饿的情况是非常重要的。
preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
//用于表示 Goroutine(g 结构体)在被抢占时的行为。当 preemptStop 字段为 true 时,表示该 Goroutine 在被抢占时会转变为 _Gpreempted 状态。
//_Gpreempted 是一个特殊的 Goroutine 状态,表示该 Goroutine 已被抢占。
//相反,当 preemptStop 字段为 false 时,表示该 Goroutine 在被抢占时只会被调度器暂时挂起(deschedule),而不会转变为 _Gpreempted 状态。
//_Gpreempted 状态的 Goroutine 会在恢复执行时立即被中断,以便让调度器可以重新调度它。
//这样可以确保在 Goroutine 被抢占后能够尽快恢复到调度器的控制下。
//preemptStop 字段的设置在一些特定的调度和抢占策略中是有意义的。
//例如,在 M:N 调度模型中,当 Goroutine 被抢占时,它可能会在另一个 M 上继续执行。
//设置 preemptStop 字段为 true 可以确保被抢占的 Goroutine 被中断并转变为 _Gpreempted 状态,以便在其他 M 上重新调度。
preemptStop bool // transition to _Gpreempted on preemption; otherwise, just deschedule
//用于表示在同步安全点(synchronous safe point)时是否缩小(shrink) Goroutine 的堆栈.
//同步安全点是指在 Goroutine 执行期间的某个特定位置,它是一个安全的点,可以进行堆栈操作而不会影响 Goroutine 的一致性。
//在同步安全点,Goroutine 的栈可以进行调整和缩小。
//当 preemptShrink 字段为 true 时,表示该 Goroutine 在同步安全点时可以进行堆栈的缩小操作。
//堆栈缩小可以释放不再使用的堆栈空间,从而减少内存的消耗。
//堆栈的缩小操作通常是由运行时系统自动触发的,而 preemptShrink 字段可以用于控制该操作的行为。
//在同步安全点时,如果 preemptShrink 字段为 true,则运行时系统会考虑对 Goroutine 的堆栈进行缩小操作。
preemptShrink bool // shrink stack at synchronous safe point
// asyncSafePoint表示 Goroutine(g 结构体)是否在异步安全点(asynchronous safe point)被停止.
//当 asyncSafePoint 字段为 true 时,表示该 Goroutine 在异步安全点被停止。
//异步安全点是 Goroutine 停止的一种特殊状态,可能涉及到对帧的扫描和处理,例如垃圾回收器的工作。
//在异步安全点停止的情况下,Goroutine 的堆栈上可能存在一些帧,这些帧上的指针信息可能不是精确的,可能会对某些运行时操作和调试工具产生影响。
asyncSafePoint bool
//用于表示在出现意外的故障地址时,是否触发 panic(而不是崩溃)
//注意panic 和crash这两个概念在golang中是有区别的
//panic是一种可被程序捕获和处理的运行时错误,它会导致程序从当前函数传播到上层函数,并执行延迟函数。
//而 crash 是指程序遇到无法恢复的严重错误,导致程序立即终止,并触发操作系统级别的错误处理机制。
//在正常情况下,当程序访问到一个无效或未映射的内存地址时,运行时系统会引发一个崩溃(crash),导致程序终止。
//这种崩溃是为了保护程序免受无效内存访问的影响。
//但是,当 paniconfault 字段设置为 true 时,表示当程序遇到意外的故障地址时,会触发一个 panic,而不是直接崩溃。
//通常情况下,paniconfault 字段不应该被设置为 true,除非你需要在出现意外的故障地址时进行特殊处理,
//例如在调试或诊断过程中,设置为 true 可以捕获并处理无效内存访问的情况,使程序能够继续执行。
paniconfault bool // panic (instead of crash) on unexpected fault address
//表示 Goroutine(g结构体)的堆栈是否已经被垃圾回收器扫描完成.
//在进行垃圾回收时,垃圾回收器需要遍历并扫描 Goroutine 的堆栈,以确定堆栈上的对象是否可以被回收。
//当 gcscandone 字段为 true 时,表示该 Goroutine 的堆栈已经被垃圾回收器完全扫描过了。
//这意味着垃圾回收器已经检查了堆栈上的所有对象,并进行了必要的回收操作。
//gcscandone 字段的设置受到 Goroutine 状态中的 _Gscan 位的保护。
//_Gscan 位是 Goroutine 状态的一部分,用于标记 Goroutine 是否正在被垃圾回收器扫描。
//这种保护机制确保了在垃圾回收过程中,只有在进行堆栈扫描的 Goroutine 才能更新 gcscandone 字段。
//这样可以避免其他并发的 Goroutine 在未完成扫描的情况下访问或修改 gcscandone 字段,确保了数据的一致性
gcscandone bool // g has scanned stack; protected by _Gscan bit in status
//用于表示 Goroutine 是否禁止分割堆栈的标识.
//当 throwsplit 字段为 true 时,表示该 Goroutine 被标记为禁止堆栈分割。堆栈分割是指在 Goroutine 运行时自动增长和收缩堆栈的过程。
//Golang 的运行时系统为每个 Goroutine 分配一块固定大小的堆栈空间。
//当 Goroutine 的堆栈空间不足以容纳当前的执行需求时,堆栈会自动增长,以满足更大的堆栈需求。
//类似地,当 Goroutine 的堆栈空间超过一定的阈值时,堆栈会自动收缩,以释放不再需要的内存。
//然而,在某些情况下,我们希望禁止 Goroutine 的堆栈分割,要求 Goroutine 在整个生命周期中保持固定大小的堆栈.
//通过将 throwsplit 字段设置为 true,我们可以指示运行时系统不对该 Goroutine 进行堆栈的自动增长和收缩操作。
//这可以确保 Goroutine 的堆栈始终保持固定的大小。
throwsplit bool // must not split stack
//表示是否存在指向该 Goroutine 堆栈的未锁定通道(channels)
//当 activeStackChans 字段为 true 时,表示存在指向该 Goroutine 堆栈的未锁定通道。
//换句话说,有其他 Goroutine 可能通过通道访问当前 Goroutine 的堆栈。
//在 Golang 中,通道是一种用于 Goroutine 之间通信和同步的重要机制。
//当一个 Goroutine 将值发送到通道或从通道接收值时,它会与其他 Goroutine 进行通信。这可能涉及对通道的锁定和解锁操作。
//在某些情况下,通道可以指向其他 Goroutine 的堆栈,这意味着其他 Goroutine 可以通过通道访问当前 Goroutine 的堆栈数据。
//当 activeStackChans 字段为 true 时,表示当前 Goroutine 存在这样的未锁定通道。
//为了保护堆栈数据的完整性,当需要对当前 Goroutine 的堆栈进行复制(例如,在进行垃圾回收时),需要先获取通道的锁,
//以防止其他 Goroutine 访问该堆栈。通过锁定通道,可以确保堆栈复制过程中的数据一致性。
activeStackChans bool
// 用于表示 Goroutine 即将在 chansend(通道发送)或 chanrecv(通道接收)操作上进行休眠。
//它用于标记一个不安全点,以便进行堆栈缩减。
//我们知道,通道是 Golang 中用于 Goroutine 之间进行通信的机制。
//在某些情况下,当 Goroutine 需要在 chansend 或 chanrecv 操作上等待时,它会进入休眠状态,暂时停止执行,直到满足相应的条件。
//当 Goroutine 即将在 chansend 或 chanrecv 操作上进行休眠时,parkingOnChan 字段被设置为 true。
//这表示当前 Goroutine 正在等待通道操作完成,并准备进入休眠状态。
//为了进行堆栈的收缩(stack shrinking),即释放不再需要的堆栈空间,需要在安全点(safe point)执行堆栈收缩操作。
//但是,在 Goroutine 进入休眠状态之前,堆栈收缩是不安全的,因为收缩操作可能会干扰到正在进行的通道操作。
//通过标记 parkingOnChan 字段为 true,可以将 Goroutine 的休眠操作作为不安全点进行标记。
//在运行时系统的堆栈收缩机制中,会遇到这个标记,并在安全点之前等待 Goroutine 的休眠操作完成后再进行堆栈的收缩。
parkingOnChan atomic.Bool
//用于忽略竞争检测(race detection)事件。
//竞争检测是Golang的一项重要功能,用于检测并发程序中的数据竞争情况,即多个goroutine同时访问和修改共享数据的情况。
//竞争检测的目的是帮助开发者发现并修复潜在的并发问题。
//我们可以使用go build/run时指定-race参数进行静态检测的调试,在go文件首行增加注释行 `// +build !race`可以告诉编译器不需要开启静态检测
raceignore int8 // ignore race detection events
//表示在当前 Goroutine(g 结构体)上是否已经发出了与系统调用(syscall)相关的跟踪事件.
//标记当前 Goroutine 是否已经被追踪,并且与系统调用相关的事件已经被发出。
//当 sysblocktraced 字段为 true 时,表示当前 Goroutine 已经被追踪,并且与系统调用相关的跟踪事件(EvGoInSyscall)已经被发出。
//系统调用是指程序在执行期间通过操作系统提供的接口进行的一种操作,例如文件读写、网络通信等。
//在跟踪和调试的过程中,记录 Goroutine 进入和离开系统调用的事件可以提供有关程序行为的详细信息。
sysblocktraced bool // StartTrace has emitted EvGoInSyscall about this goroutine
//用于指示是否正在跟踪该 Goroutine 的调度延迟统计信息。
//调度延迟是指在多个 Goroutine 并发执行时,从一个 Goroutine 切换到另一个 Goroutine 的时间延迟。
//Golang 的运行时系统具有用于跟踪和统计调度延迟的功能,以提供有关 Goroutine 的调度性能和行为的信息。
//当 tracking 字段为 true 时,表示正在跟踪该 Goroutine 的调度延迟统计信息。
//这意味着运行时系统会记录和统计与该 Goroutine 相关的调度延迟数据。
//跟踪调度延迟可以帮助开发人员了解 Goroutine 的调度行为,识别潜在的性能问题和瓶颈,并进行性能调优。
//通过收集和分析调度延迟统计信息,可以优化 Goroutine 的调度策略和资源利用,提高程序的并发性能。
tracking bool // whether we're tracking this G for sched latency statistics
//用于决定是否跟踪该 Goroutine一个序列号.
//在 Golang 的运行时系统中,跟踪(tracking)是一项资源密集型的操作,会占用额外的内存和计算资源。
//为了优化性能,运行时系统不会对所有的 Goroutine 进行跟踪。相反,它使用 trackingSeq 字段来决定是否对该 Goroutine 进行跟踪。
//trackingSeq 字段用于存储一个序列号(sequence number),该序列号用于标识该 Goroutine。
//运行时系统会根据一些策略和算法,根据 trackingSeq 字段的值来决定是否跟踪该 Goroutine。
//通过使用 trackingSeq 字段,运行时系统可以有选择地对一部分 Goroutine 进行跟踪,以平衡性能和资源的消耗。
//对于没有跟踪的 Goroutine,它们的 trackingSeq 字段可能会被设置为一个特定的值(例如 0),表示不进行跟踪。
trackingSeq uint8 // used to decide whether to track this G
//用于记录最后一次开始跟踪该 Goroutine 的时间戳
trackingStamp int64 // timestamp of when the G last started being tracked
//用于记录 Goroutine 处于可运行状态的时间。该字段只在启用了 Goroutine 跟踪功能时使用.
//Goroutine 的可运行状态是指它已经准备好并可以被调度执行的状态。
//当 Goroutine 处于可运行状态时,它可能正在等待调度器选择执行它。
//runnableTime 字段记录了 Goroutine 处于可运行状态的累计时间。
//当 Goroutine 开始运行时,即被调度器选择执行时,runnableTime 字段会被清零。
//然后,在 Goroutine 再次变为可运行状态之前,runnableTime 字段会记录 Goroutine 处于可运行状态的时间。
//该字段的目的是帮助收集有关 Goroutine 的调度统计信息,例如计算 Goroutine 的可运行时间占据总运行时间的比例。
//通过跟踪 Goroutine 的 runnableTime,可以了解 Goroutine 在调度器中的活跃程度和执行时间。
runnableTime int64 // the amount of time spent runnable, cleared when running, only used when tracking
//用于在跟踪中记录系统调用返回时的 CPU 时间戳。
//当 Goroutine 执行系统调用并返回时,runtime 系统会记录 sysexitticks 字段的值,以标记系统调用返回时的 CPU 时间戳。
//这个时间戳可以用于跟踪工具来分析系统调用的性能和行为。
//通过记录系统调用返回的时间戳,可以分析系统调用的耗时以及与其他事件之间的时间关系。这对于性能调优和故障排查非常有用。
sysexitticks int64 // cputicks when syscall has returned (for tracing)
//用于跟踪事件的顺序号生成器。在 Golang 中,可以使用 trace 包来进行性能分析和跟踪应用程序的执行过程。
//跟踪事件是指在应用程序的执行过程中发生的一系列重要事件,例如函数调用、系统调用、内存分配等。
//这些事件可以帮助开发人员了解应用程序的执行流程、资源使用情况和性能瓶颈。
//traceseq 字段用于生成跟踪事件的顺序号。
//每当需要生成一个新的跟踪事件时,运行时系统会将 traceseq 字段的值作为该事件的顺序号,并将 traceseq 字段递增。
//这样可以确保跟踪事件具有唯一的顺序号,以便在跟踪分析工具中进行准确的时间线重建和分析。
traceseq uint64 // trace event sequencer
//用于记录最后一个发出事件的 P(处理器)与该 Goroutine 之间的关联。
//在 Golang 的运行时系统中,P 是一个处理器,用于执行 Goroutine。
//当 Goroutine 需要执行时,调度器会将其分配给一个 P,以便在该处理器上执行。
//tracelastp 字段用于跟踪与该 Goroutine 相关联的最后一个发出事件的 P。
//这个字段的值是一个指向 P 的指针,指示最后一个与该 Goroutine 相关的事件发生的处理器。
//跟踪与 Goroutine 相关的最后一个事件的 P 可以在调试和跟踪工具中使用。
//通过记录最后一个事件的 P,可以了解 Goroutine 最后一次在哪个处理器上执行,有助于分析调度行为和性能特征。
tracelastp puintptr // last P emitted an event for this goroutine
//用于存储与当前 Goroutine 相关联的锁定的 m(调度器线程)。
//当 Goroutine 成功获得一个锁时,lockedm 字段会被设置为持有该锁的 m 的指针。
//这表示当前 Goroutine 正在持有特定的锁,并且与该锁关联的 m 负责执行该 Goroutine。
//通过跟踪 lockedm 字段,运行时系统可以了解当前 Goroutine 所持有的锁和关联的调度器线程。
//这对于锁的调度和资源的管理是至关重要的,确保并发访问的正确性和一致性。
lockedm muintptr
//用于存储与 Goroutine 相关的信号信息。
//信号是在操作系统级别产生的事件或通知,可能包含中断、错误、终止等。
//这些信号可以由操作系统、其他 Goroutine 或用户代码发送。
//当一个 Goroutine 接收到信号时,runtime 系统会将相应的信号信息存储在 sig 字段中。
//这样,Goroutine 可以在适当的时候对接收到的信号进行处理。
sig uint32
//用于存储用于写入数据的缓冲区。
//writebuf 字段通常用于在 Goroutine 执行网络通信或文件操作时,临时存储要发送或写入的数据。
//它提供了一个缓冲区,用于暂时保存待写入的字节数据,然后批量地发送或写入到相应的目标。
//由于网络通信和文件操作涉及到读写效率和数据传输的优化,使用缓冲区可以减少系统调用的频率,提高数据传输的效率。
//通过将数据先写入 writebuf 字段中的缓冲区,可以在一定程度上减少对底层资源的频繁访问和操作
writebuf []byte
//用于存储与 Goroutine 相关的信号代码(Signal Code)信息。
//sigcode0 字段用于表示 Goroutine 接收到的信号的代码。信号代码是与特定信号相关联的标识符,用于描述信号的类型和原因。
//当一个 Goroutine 接收到信号时,runtime 系统会将相应的信号代码存储在 sigcode0 字段中。
//这样,Goroutine 可以根据信号代码来确定如何处理接收到的信号。
sigcode0 uintptr
//也是用于存储与 Goroutine 相关的信号代码(Signal Code)信息。
//sigcode0 用于存储接收到的信号的主要代码,而 sigcode1 则用于存储与信号相关的附加信息或辅助代码.
//sigcode0 字段通常用于表示主要的信号代码,用于标识接收到的信号的类型或操作。
//例如,它可能用于表示 SIGSEGV(段错误)信号、SIGILL(非法指令)信号等。
//而 sigcode1 字段则通常用于存储与信号相关的附加信息或辅助代码,用于提供更多关于信号的上下文或详细信息。
//例如,它可能用于存储导致段错误的内存地址、非法指令的指令地址等。
sigcode1 uintptr
//用于存储与信号相关的程序计数器(Program Counter).
//程序计数器是一种特殊的寄存器,用于存储当前正在执行的指令的地址。它指示了正在执行的代码的位置,是处理器执行下一条指令的地址。
//在信号处理的上下文中,当发生信号时,操作系统会中断正在执行的程序,并将控制权转移到信号处理程序中。
//sigpc 字段用于存储与信号相关的程序计数器的值,即表示信号发生时的指令地址。
//sigpc 字段的值可以用于追踪和记录信号发生的位置,帮助调试和诊断信号处理相关的问题。
//通过记录程序计数器的值,可以知道在发生信号的时候程序执行到了哪个位置,从而帮助定位可能出现的错误或异常情况。
sigpc uintptr
//gopc用于存储该goroutine的go语句的程序计数器(program counter)值。
//程序计数器是一个特殊的寄存器,用于存储当前正在执行的指令的地址。在Golang中,每个goroutine都有一个与之关联的程序计数器。
//当一个新的goroutine被创建时,runtime系统会记录并存储该goroutine的创建点信息,即创建该goroutine的go语句的程序计数器值。
//这个信息可以通过g结构体中的gopc字段来访问。
//通过pc字段,我们可以追踪和识别每个goroutine的创建点,即调用go语句的位置。这对于调试和分析goroutine的创建和执行过程非常有用。
//它可以帮助我们了解goroutine的起源和上下文,从而更好地理解程序的并发行为。
//需要注意的是,gopcpc字段的值是一个指向具体指令的地址。
//因为在Golang中的goroutine创建语句通常是go函数调用,所以pc字段的值可以理解为创建goroutine的go语句所在函数的程序计数器值。
//通过使用pc字段,我们可以了解和跟踪goroutine的创建点,从而更好地理解程序的执行和并发模式。
//这对于调试、性能优化和并发问题排查都非常有帮助。
gopc uintptr // pc of go statement that created this goroutine
//用于存储创建当前 Goroutine 的祖先 Goroutine 的信息.
//例如祖先 Goroutine 的 ID、栈跟踪信息等。
//ancestors 字段在以下情况下被使用:
//1.只有在启用了 debug.tracebackancestors 标志时才会被使用。
//2. 当需要追踪和记录创建当前 Goroutine 的祖先 Goroutine 信息时,运行时系统会将祖先 Goroutine 的相关信息存储在 ancestors 字段指向的切片中。
//debug.tracebackancestors 是一个调试标志,用于启用在 Goroutine 栈跟踪信息中记录祖先 Goroutine 信息的功能。
//当该标志启用时,运行时系统会在 Goroutine 的栈跟踪信息中包含创建该 Goroutine 的祖先 Goroutine 的信息。
//通过使用 ancestors 字段和 debug.tracebackancestors 标志,Golang 的运行时系统可以提供更丰富的 Goroutine 栈跟踪信息,
//包括祖先 Goroutine 的相关信息。这有助于调试和定位 Goroutine 的创建和调用关系,特别是在复杂的并发程序中。
ancestors *[]ancestorInfo // ancestor information goroutine(s) that created this goroutine (only used if debug.tracebackancestors)
//用于存储Goroutine 函数的起始指令的地址.在 Golang 中,每个 Goroutine 都与一个特定的函数关联,该函数定义了 Goroutine 的入口点和要执行的代码。
//startpc 字段记录了与该 Goroutine 关联的函数的起始指令地址。
//通过 startpc 字段,可以得到 Goroutine 函数的起始位置,也就是 Goroutine 执行的起始点。
//这对于追踪和调试 Goroutine 的执行流程和代码路径非常有用。
//可以根据 startpc 字段的值,定位到 Goroutine 执行的具体代码位置。
startpc uintptr // pc of goroutine function
//用于存储与竞态检测(race detection)相关的上下文信息。
//竞态检测是一种用于检测并发程序中可能存在的数据竞态(data race)问题的技术。
//数据竞态是指多个 Goroutine 同时访问共享数据,并且至少有一个访问是写操作,而且这些访问没有适当的同步操作。
//竞态检测的目标是在程序运行时检测和报告这些潜在的并发错误。
//racectx 字段用于存储与竞态检测相关的上下文信息。
//这些上下文信息可能包括跟踪竞态检测的状态、报告错误的位置、数据访问的历史等。
//具体的上下文信息和使用方式取决于竞态检测工具和运行时配置。
racectx uintptr
// waiting 字段是一个指向 sudog 结构体的指针,用于存储当前 Goroutine 等待的 sudog 结构体。
//sudog(也称为唤醒原语,synchronization descriptor)是一种在 Golang 的运行时系统中用于实现 Goroutine 的等待和唤醒机制的数据结构。
//当一个 Goroutine 需要等待某个条件满足时,它会进入等待状态,并将相关的 sudog 结构体添加到 waiting 字段中。
//waiting 字段存储了当前 Goroutine 正在等待的 sudog 结构体的指针。
//这些 sudog 结构体通常与锁、通道或其他同步原语相关联,并用于在特定条件满足时唤醒 Goroutine。
//waiting 字段中的 sudog 结构体按照特定的锁顺序进行排列,以确保等待和唤醒的正确顺序。
//当条件满足时,其他 Goroutine 可能会通过唤醒等待队列中的 sudog 结构体来通知等待中的 Goroutine 继续执行。
waiting *sudog // sudog structures this g is waiting on (that have a valid elem ptr); in lock order
//用于存储与 cgo相关的回溯(traceback)上下文信息.
//在 Golang 中,cgo 是一种机制,允许在 Go 代码中调用 C 语言代码或使用 C 语言库。
//当使用 cgo 进行混合编程时,Golang 的运行时系统可以跟踪与 cgo 相关的调用链,以便在发生问题或错误时进行回溯和调试。
//cgoCtxt 字段是一个 uintptr 类型的切片,其中的每个元素表示 cgo 调用链中的一个函数的程序计数器(Program Counter)值。
//程序计数器表示当前正在执行的指令的地址,通过记录 cgo 调用链中每个函数的程序计数器,可以构建完整的回溯上下文,
//帮助定位和追踪与 cgo 相关的问题。
cgoCtxt []uintptr // cgo traceback context
//用于存储与性能分析器(profiler)相关的标签信息.这些标签可以用于标识和组织性能数据
labels unsafe.Pointer // profiler labels
//用于缓存 time.Sleep 操作所使用的定时器。
//time.Sleep 是 Golang 中用于暂停执行一段时间的函数。它常用于控制 Goroutine 的执行间隔或实现延迟操作。
//为了避免每次调用 time.Sleep 都创建新的定时器,runtime 使用 timer 字段来缓存一个定时器,以便重复使用。
//timer 结构体包含了与定时器相关的信息,如过期时间、触发时间等。
//通过缓存一个定时器,可以避免在每次调用 time.Sleep 时都进行定时器的创建和销毁,从而提高性能和效率。
timer *timer // cached timer for time.Sleep
//用于表示当前 Goroutine是否参与了 select 操作,并且是否存在竞争的情况
//在 Golang 中,select 语句用于在多个通道操作之间进行非阻塞选择。
//当一个 Goroutine执行 select 语句时,它会尝试在多个通道上进行操作,并等待其中一个操作完成。
//如果多个 Goroutine同时执行 select,并且某个操作同时满足多个 Goroutine 的条件,就会发生竞争。
//selectDone 字段通过使用 atomic.Uint32 类型的原子操作,表示当前 Goroutine是否参与了 select 操作,并且是否存在竞争的情况。
//如果 selectDone 字段的值为非零(1),表示当前 Goroutine已经参与了 select 操作,并且可能存在竞争的情况。
//竞争条件可能导致多个 Goroutine同时选择同一个操作,进而引发不确定的行为。
//通过使用 selectDone 字段,可以标记当前 Goroutine参与了 select 操作,并通知其他 Goroutine不再进行竞争,从而避免竞争问题。
selectDone atomic.Uint32 // are we participating in a select and did someone win the race?
//在Golang的runtime包中,g结构体中的goroutineProfiled字段用于指示当前正在进行的goroutine分析(goroutine profiling)的堆栈状态。
//goroutine profiling是一种性能分析工具,用于收集和分析正在运行的goroutine的信息,以了解其调用关系、执行时间和内存消耗等方面的情况。
//goroutineProfiled字段用于跟踪和记录这种分析的状态。
//具体来说,goroutineProfiled字段的值表示该goroutine的堆栈状态是否正在进行分析。
//如果goroutineProfiled为true,则表示该goroutine的堆栈正在进行分析,即正在进行goroutine profiling。
//反之,如果goroutineProfiled为false,则表示该goroutine的堆栈不在当前进行分析。
//通过将goroutineProfiled字段与其他分析工具相关的字段进行配合使用,可以在分析过程中选择性地收集和分析特定的goroutine。
//这有助于精确控制分析的范围和粒度,以便更好地理解和优化程序的性能。
//需要注意的是,goroutineProfiled字段是在运行时系统内部维护的,并且对用户代码是不可见的。它是用于内部性能分析和调试工具的一部分。
//通过使用goroutineProfiled字段,Golang的运行时系统能够准确地追踪和记录正在进行goroutine profiling的goroutine堆栈的状态,以提供更有效的性能分析和优化手段。
goroutineProfiled goroutineProfileStateHolder
//gcAssistBytes字段的值为正数时,表示Goroutine具有一定的垃圾回收辅助信用,可以在不进行辅助工作的情况下继续分配内存。
//当Goroutine分配内存时,gcAssistBytes字段会递减。
//而当gcAssistBytes字段的值为负数时,表示Goroutine需要通过执行扫描工作来纠正负债。
//这意味着Goroutine需要帮助进行垃圾回收,以还清负债。
//gcAssistBytes字段的设计考虑了性能的因素。通过以字节数为单位进行计算和跟踪,可以更快速地更新和检查是否存在垃圾回收负债。
//而gcAssistBytes字段与扫描工作负债之间的对应关系是由辅助比例(assist ratio)来确定的。
//辅助比例决定了gcAssistBytes字段与扫描工作负债之间的转换关系。
//需要注意的是,gcAssistBytes字段是在运行时系统内部维护的,并且对用户代码是不可见的。
//它是用于内部垃圾回收调度和性能优化的一部分。
//通过使用gcAssistBytes字段,Golang的运行时系统可以根据Goroutine的内存分配情况来自动调整垃圾回收辅助工作的执行。
//这样可以有效地管理内存和垃圾回收的负载,以提高程序的执行效率。
gcAssistBytes int64
}
type gobuf struct {
//堆栈指针(Stack Pointer),表示Goroutine当前的堆栈位置
sp uintptr // 堆栈指针
//程序计数器(Program Counter),表示Goroutine当前执行的指令地址
pc uintptr // 程序计数器
//当前Goroutine的g结构体指针,用于快速访问Goroutine的其他信息
g guintptr // 当前Goroutine的g结构体指针
// 保存的上下文指针,用于切换Goroutine时保存和恢复Goroutine的上下文信息
ctxt unsafe.Pointer
// 返回值(Return Value),用于保存函数调用返回时的返回值
ret sys.Uintreg
// 返回地址(Link Register),表示函数调用返回时需要跳转的地址
lr uintptr
//基指针(Base Pointer),用于指示当前函数的栈帧
bp uintptr
}
M 的底层结构#
// m 代表工作线程,保存了自身使用的栈信息
type m struct {
//g0 字段存储了具有调度堆栈的 Goroutine 的信息。g0被称为工作线程(也叫内核线程)。
// 执行用户 goroutine 代码时,使用用户 goroutine 自己的栈,因此调度时会发生栈的切换
//它是一个指向 g 结构体的指针,该结构体包含了 Goroutine 的状态、堆栈指针、程序计数器等信息。
//g0 是一个特殊的 Goroutine,它用于管理调度和协程的创建、销毁和切换。
//通过使用 g0 字段,运行时系统可以管理和调度具有调度堆栈的 Goroutine。
//它包含了关键的上下文信息,使得 Goroutine 能够在 M(线程)上正确地执行和切换
g0 *g // goroutine with scheduling stack
//morebuf用于存储在执行 "morestack" 操作时传递给更大栈的参数。
//"morestack" 是一个内部函数,用于处理 Goroutine 的栈扩展操作。
//当 Goroutine 的栈空间不足以容纳当前的执行需求时,会触发 "morestack" 操作来分配更大的栈空间。
//morebuf 字段用于保存传递给 "morestack" 操作的参数 gobuf。
//前面介绍过,gobuf 是一个结构体,用于描述 Goroutine 的栈和寄存器等信息。
//通过将 gobuf 参数存储在 morebuf 字段中,可以在执行 "morestack" 操作时提供必要的参数。
//这样,"morestack" 函数就能够了解当前 Goroutine 的栈和寄存器状态,并进行栈扩展操作。
morebuf gobuf // gobuf arg to morestack
//用于在 ARM 架构中进行除法和取模运算的分母.
//在 ARM 架构中,整数除法和取模运算(即除法的余数计算)通常是使用特定的分母进行计算,以提高性能和效率。
//divmod 字段用于存储在进行这些运算时使用的分母值。
//这个值是在编译阶段由编译器和链接器(liblink)确定,并且对于特定的 ARM 架构是已知的。
divmod uint32 // div/mod denominator for arm - known to liblink
//用于对齐下一个字段的内存布局,使其在 8 字节边界上对齐
//在某些架构中,对于性能和内存访问的优化,数据在内存中需要按照特定的对齐方式存储。
//例如,有些处理器要求 8 字节对齐,即某个数据的地址必须是 8 的倍数。
//为了确保下一个字段在 8 字节边界上对齐,_ 字段被插入到结构体中作为填充字段。
//该字段本身不存储任何实际的数据,它只是占用一定的内存空间,以使下一个字段在对齐位置上对齐。
_ uint32 // align next field to 8 bytes
// 用于提供给调试器使用的进程标识符。
//procid 字段主要是为了调试器能够识别和跟踪与特定 M(Machine)关联的进程。
//调试器是开发和调试程序时常用的工具,它可以与运行中的程序交互并提供调试信息。
//procid 字段的具体值可以用于唯一标识与 M 关联的进程,并提供给调试器使用。
//通过 procid 字段,调试器可以根据这个标识符来定位和跟踪特定的进程,以便进行调试操作和收集调试信息。
procid uint64 // for debuggers, but offset not hard-coded
// gsignal 字段是一个指向 g 结构体的指针,用于表示处理信号的 Goroutine。
//在操作系统中,信号是用于通知进程发生了某个事件或需要进行某个操作的一种异步通知机制。
//Golang 运行时系统中的 gsignal 字段用于保存负责处理信号的 Goroutine 的信息。
//gsignal 字段存储了一个特殊的 Goroutine,它负责处理与运行时系统相关的信号。
//当发生信号时,运行时系统会将信号传递给 gsignal 所指定的 Goroutine,并由该 Goroutine 进行相应的信号处理操作。
//通过使用 gsignal 字段,Golang 的运行时系统可以将信号处理的责任委托给特定的 Goroutine,以便进行信号处理和相关操作。
//这有助于将信号处理与其他 Goroutine 的执行分离,提高信号处理的可靠性和响应性。
gsignal *g // signal-handling g
//用于表示为信号处理分配的栈空间.
//在 Golang 的运行时系统中,处理信号的操作通常需要在一个独立的栈上执行,以保证信号处理的可靠性和安全性。
//在Golang的runtime包中,gsignal stack(信号堆栈)是用来处理操作系统信号的栈空间。
//当操作系统发送信号给Go程序时,信号处理函数会在gsignal stack上运行。
//这个栈是独立于普通的goroutine栈的,用于专门处理信号的相关操作。
//gsignal stack的主要作用是提供一个独立的执行环境,确保信号处理函数能够正常运行而不受其他goroutine的影响。
//在处理信号期间,runtime会禁止抢占和栈扩展,以确保信号处理函数的运行不会被干扰。
//由于信号处理函数需要尽可能地简洁和高效,gsignal stack的大小是固定的,并且相对较小。
//这是因为在信号处理期间,只能执行少量的操作,例如发送或接收信号、终止程序等。过多的操作可能会带来不可预知的问题。
//需要注意的是,gsignal stack不同于goroutine栈,它是专门用于处理信号的,而goroutine栈则用于正常的程序执行。
//这样的设计可以有效地隔离信号处理函数和普通程序逻辑,提高信号处理的可靠性和安全性。
goSigStack gsignalStack // Go-allocated signal handling stack
//表示 M 的信号掩码,用于控制哪些信号可以中断 M 的执行
sigmask sigset // storage for saved signal mask
//用于存储线程本地存储(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()
//表示当前正在执行的 Goroutine。curg 是一个指向 g 结构体的指针。在 M 上运行的 Goroutine 会存储在 curg 中。
curg *g // current running goroutine
//用于表示在发生致命信号(fatal signal)时正在执行的 Goroutine。
//ghtsig 字段的值是一个 guintptr 类型的整数,通常是一个指针值。
//它指向正在执行的 Goroutine 。
//通过使用 caughtsig 字段,Golang 的运行时系统可以在发生致命信号时记录正在执行的 Goroutine,以便进行相应的处理和调试。
//这有助于在程序崩溃时确定造成崩溃的 Goroutine。
caughtsig guintptr // goroutine running during fatal signal
//用于表示与当前 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 的 P。当 M 执行完当前的 P 上的 Goroutine 后,会切换到 nextp 所指定的 P 上.
nextp puintptr
//用于保存在执行系统调用之前与 M(Machine)相关联的 P(Processor)。
//当 M 需要执行系统调用时,它会从当前关联的 P 上分离,并保存原始的 P 地址到 oldp 字段。
//这样,在系统调用完成后,M 可以重新关联到原始的 P 上,以继续执行 Go 代码。
//puintptr 是一个 uintptr 类型的别名,用于保存指向 P 的地址。
//通过使用 oldp 字段,M 可以在系统调用执行期间保存并恢复与 P 的关联关系。
oldp puintptr // the p that was attached before executing a syscall
//表示 M 的唯一标识符。每个 M 都有一个唯一的标识符,用于区分不同的 M。
id int64
//用于指示当前 M(Machine)是否正在执行 malloc 操作。
//mallocing 字段的值为 1 表示当前 M 正在执行 malloc 操作,即分配内存。
//如果 mallocing 字段的值为 0,则表示当前 M 不在执行 malloc 操作。
//malloc 是动态内存分配的一种方式,在 Golang 中,它通常用于分配堆上的内存空间,用于存储动态创建的对象或数据结构。
//通过使用 mallocing 字段,运行时系统可以跟踪每个 M 是否正在执行 malloc 操作。
//这对于调度和资源管理非常重要,因为在执行 malloc 操作期间,运行时系统可能需要采取特殊的处理措施,
//如处理内存分配失败、执行垃圾回收等。
mallocing int32
//用于指示当前 M(Machine)是否正在执行 panic 操作。
//throwing 字段的值为 true 表示当前 M 正在执行 panic 操作,即触发异常。
//通过使用 throwing 字段,运行时系统可以跟踪每个 M 是否正在执行 panic 操作。
//这对于异常处理、调度和资源管理非常重要,因为在执行 panic 操作期间,运行时系统可能需要采取特殊的处理措施,
//如处理异常、执行栈展开等。
throwing throwType
//用于指示是否禁用抢占(preemption)并保持当前 Goroutine 在该 M(Machine)上继续运行。
//preemptoff 字段的值如果不为空字符串(""),则表示禁用抢占并保持当前 Goroutine 在该 M 上继续执行。
//如果 preemptoff 字段的值为空字符串,则表示抢占机制处于启用状态。
//抢占是指在多线程编程中,当一个线程正在执行时,操作系统或调度器中断该线程的执行,
//并将 CPU 时间片分配给其他优先级更高的线程。
//这是为了确保公平的线程调度和避免某个线程长时间占用 CPU 资源。
//通过使用 preemptoff 字段,Golang 的运行时系统可以临时禁用抢占机制,并保持当前 Goroutine 在该 M 上继续执行。
//这可以用于一些特殊的场景,例如在关键代码段或特定的调试操作中禁用抢占。
preemptoff string // if != "", keep curg running on this m
//用于表示当前 M(Machine)持有的锁的数量。
//当 M 获取一个锁时,locks 字段的值会增加;当 M 释放一个锁时,locks 字段的值会减少。
//通过使用 locks 字段,运行时系统可以跟踪每个 M 持有的锁的数量。这对于调度和资源管理非常重要,因为锁的获取和释放可能会影响 Goroutine 的调度和执行
locks int32
//用于表示当前 M(Machine)是否正在退出或终止的状态。
//dying 字段的值为非零表示 M 正在退出或终止的过程中。
//当 M 准备退出时,它会将 dying 字段的值设置为非零,并且在退出前进行必要的清理操作。
//M 的退出通常发生在运行时系统关闭或 M 不再需要的情况下。在退出过程中,M 可能会释放资源、关闭连接、执行清理操作等。
//通过使用 dying 字段,运行时系统可以跟踪每个 M 是否处于退出状态,并在必要时进行适当的清理和处理。
//需要注意的是,dying 字段是在运行时系统内部使用的,对于用户代码来说是不可见的。它用于运行时系统的 M 管理和资源清理。
dying int32
//表示与 M 相关的分析器的采样频率
profilehz int32
//表示 M 当前是否处于自旋状态。
//自旋是一种等待的方式,当 M 需要等待一些条件满足时,会快速地尝试获取锁或资源,而不进行阻塞。
spinning bool // m is out of work and is actively looking for work
//用于表示当前 M(Machine)是否被阻塞在一个 note 上。
//note 是运行时系统中用于协调和同步 Goroutine 的一种基本机制。
//当一个 Goroutine 需要等待某个事件或条件满足时,它会被阻塞在一个 note 上,直到该事件发生或条件满足。
//blocked 字段的值为 true 表示当前 M 正在被阻塞在一个 note 上。
//通过使用 blocked 字段,运行时系统可以跟踪每个 M 是否被阻塞,并相应地进行调度和资源管理。
//当一个 M 被阻塞时,运行时系统可以将其从可运行的 M 集合中移除,并在事件发生后重新调度该 M
blocked bool // m is blocked on a note
//newSigstack用于表示是否在 C 线程上调用了 sigaltstack 函数执行 minit(M 的初始化)。
//sigaltstack 是一个操作系统提供的函数,用于设置替代信号栈(alternate signal stack)。
//在 Golang 的运行时系统中,M 的初始化过程(称为 minit)需要在一个特定的线程上执行,通常是 C 线程。
//newSigstack 字段的值为 true 表示在执行 minit 时已在 C 线程上调用了 sigaltstack 函数,从而设置了替代信号栈。
//如果 newSigstack 字段的值为 false,则表示没有调用 sigaltstack 函数。
//通过使用 newSigstack 字段,运行时系统可以知道是否已设置了替代信号栈,并相应地调整 M 的初始化过程。
//替代信号栈的设置可以确保在处理异常或信号时,运行时系统能够在安全的上下文中执行相应的处理。
newSigstack bool // minit on C thread called sigaltstack
//用于表示当前 M(Machine)是否持有打印锁(print lock)。 //打印锁是用于保护打印操作的一种锁机制。
//在 Golang 的运行时系统中,当多个 Goroutine 同时进行打印操作时,需要使用打印锁来确保打印操作的原子性和顺序性。
//printlock 字段的值为非零表示当前 M 持有打印锁。如果 printlock 字段的值为零,则表示当前 M 不持有打印锁。
//通过使用 printlock 字段,运行时系统可以在进行打印操作时进行同步和互斥。
//当一个 M 持有打印锁时,其他 M 需要等待打印锁被释放才能进行打印操作。
printlock int8
//用于表示当前 M(Machine)是否正在执行一个 cgo 调用。
//通过使用 incgo 字段,运行时系统可以跟踪每个 M 是否正在执行 cgo 调用。
//这对于调度和资源管理非常重要,因为 cgo 调用可能涉及与 C 代码的交互、资源管理的不同策略以及调度的特殊需求。
//通过使用 incgo 字段,Golang 的运行时系统可以跟踪每个 M 是否正在执行 cgo 调用,
//并在需要时进行适当的调度和资源管理,以确保与 C 代码的交互正确进行和资源的正确释放。
incgo bool // m is executing a cgo call
//isextra用于表示当前 M(Machine)是否是额外的(extra)M。
//在 Golang 的并发模型中,M 是用于执行 Goroutine 的调度和管理的单位。
//通常情况下,每个逻辑处理器(P)关联一个 M。
//然而,当系统负载较低或需要更多并发性时,额外的 M 可以被创建来充分利用可用的处理资源。
//isextra 字段的值为 true 表示当前 M 是一个额外的 M。反之,则表示当前 M 不是额外的 M。
//通过使用 isextra 字段,运行时系统可以识别哪些 M 是额外的,并相应地进行调度和资源管理。
//额外的 M 可以在需要更多并发性时被激活,而在负载较低时可以被休眠或销毁,以优化系统的性能和资源利用率。
isextra bool // m is an extra m
//用于表示是否可以安全地释放 g0 和删除 M(Machine)。
//在 Golang 的运行时系统中,g0 是一个特殊的 Goroutine,它用于执行系统级任务和初始化操作。
//M 则是与 Goroutine 相关联的执行上下文。当 M 不再需要时,可以尝试释放与之关联的资源。
//freeWait 字段是一个原子类型的整数,用于记录释放 M 的安全状态。
//当 freeWait 字段的值为非零时,表示不安全释放 M;当 freeWait 字段的值为零时,表示可以安全释放 M。
//通过使用 freeWait 字段,运行时系统可以跟踪 M 是否可以安全地释放,并在需要时进行适当的处理。
//这通常涉及等待相关的操作完成,以确保在释放 M 之前不会出现竞争条件或数据访问冲突。
freeWait atomic.Uint32 // Whether it is safe to free g0 and delete m (one of freeMRef, freeMStack, freeMWait)
//用于表示快速随机数生成器的状态。
//fastrand 字段用于在运行时系统中生成快速的伪随机数。它是一个无锁的随机数生成器,用于产生高性能的伪随机数序列。
//通过使用 fastrand 字段,运行时系统可以在需要快速随机数生成的场景中,
//如哈希函数、并发算法、随机算法等,快速获取伪随机数。
fastrand uint64
//用于表示是否需要额外的 M(Machine)。 needextram 字段的值为 true 表示当前 M 需要额外的 M
needextram bool
//用于表示是否需要进行 Goroutine 的回溯(traceback)
//traceback 字段的值表示回溯的级别或模式。它用于控制在发生错误或异常时,运行时系统是否要生成 Goroutine 的调用栈跟踪信息。
//通过使用 traceback 字段,Golang 的运行时系统可以根据指定的级别生成适当的回溯信息,以帮助开发人员定位问题并进行调试。
//这提供了一种方便的方法来捕获和分析发生在 Goroutine 中的错误和异常。
//通过使用 traceback 字段,可以指定需要生成的回溯级别,如完整回溯、简化回溯或禁用回溯等。
//这有助于在调试和错误排查时获取有用的调用栈信息。
traceback uint8
//用于表示发生的 cgo 调用的总次数。
//通过使用 ncgocall 字段,可以统计和监控程序中的 cgo 调用次数,以便在性能优化、调试和资源管理等方面进行分析和优化。
ncgocall uint64 // number of cgo calls in total
//用于表示当前正在进行中的 cgo 调用的数量。
//cgo 调用是指在 Golang 代码中调用 C 语言函数或在 C 语言代码中调用 Golang 函数的操作。
//在进行 cgo 调用时,涉及到在 Golang 和 C 语言之间的上下文切换和数据传递。
//ncgo 字段用于跟踪当前正在进行中的 cgo 调用的数量。它反映了程序中正在执行的与 C 语言相关的操作的规模和并发性。
//通过使用 ncgo 字段,可以了解当前有多少个 cgo 调用正在同时进行,这对于了解程序中的并发性和资源利用情况是有帮助的。
//需要注意的是,ncgo 字段是在运行时系统内部使用的,对于用户代码来说是不可见的。它用于运行时系统的性能分析和资源管理。
//通过使用 ncgo 字段,Golang 的运行时系统可以提供有关当前正在进行中的 cgo 调用数量的信息,
//以帮助开发人员识别并发性问题、调整并发级别以及进行资源分配和调度。
ncgo int32 // number of cgo calls currently in progress
//用于表示 cgoCallers 是否被临时使用
//cgoCallers 是用于跟踪 cgo 调用的调用栈信息的数据结构。它用于记录 cgo 调用的调用栈,以便在需要时进行调试和跟踪。
//cgoCallersUse 字段的值为非零表示 cgoCallers 正在被临时使用。
//通过使用 cgoCallersUse 字段,运行时系统可以在 cgo 调用期间临时使用 cgoCallers 数据结构,
//并确保在使用过程中的正确性和线程安全性。
cgoCallersUse atomic.Uint32 // if non-zero, cgoCallers in use temporarily
//cgoCallers 用于记录在 cgo 调用过程中发生崩溃时的回溯信息.
//cgoCallers 结构体用于跟踪 cgo 调用期间的调用栈,以便在发生崩溃或异常时提供关于崩溃点的调用栈信息。
//通过使用 cgoCallers 字段,运行时系统可以将崩溃点与 cgo 调用相关联,并在需要时提供相关的调用栈信息,
//以帮助开发人员定位和解决崩溃问题。
cgoCallers *cgoCallers // cgo traceback if crashing in cgo call
//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
//alllink 字段是一个指向 m 结构体的指针,用于维护在 allm 列表上的所有 M(Machine)的链接。
//allm 列表是运行时系统中的一个全局列表,用于存储所有的 M。
//每个 M 在启动时会被添加到 allm 列表中,以便运行时系统能够轻松地遍历和操作所有的 M
alllink *m // on allm
//schedlink 字段是一个 muintptr 类型的字段,用于在调度器的链表中链接 M。调度器链表维护了一组可运行的 M,它们处于待调度状态。当一个 M 可以被调度执行时,它将链接到调度器链表中,以便调度器可以轮询和选择要执行的 M。schedlink 字段的作用是将当前 M 链接到调度器链表中。
schedlink muintptr
//表示在执行系统调用或其他阻塞操作期间,M 持有的锁相关的 Goroutine。
//当 M 需要阻塞时,会将此字段设置为需要持有的锁所在的 Goroutine。
lockedg guintptr
//用于记录创建当前线程的堆栈信息.
//通过使用 createstack 字段,运行时系统可以追踪创建当前 M 的线程的堆栈信息,以便在需要时进行调试和分析。
//这对于了解线程的创建和执行环境是很有帮助的。
createstack [32]uintptr // stack that created this thread.
//用于跟踪外部 LockOSThread 操作的状态。
//lockOSThread 是一个用于将当前线程锁定到当前的 M(Machine)的操作。
//当调用 lockOSThread 后,该线程将与当前的 M 关联,即只有该线程才能执行与该 M 相关的 Goroutine。
//lockedExt 字段用于跟踪外部 LockOSThread 操作的状态。当 lockedExt 的值为非零时,表示当前 M 的线程已被外部锁定;
//当 lockedExt 的值为零时,表示当前 M 的线程未被外部锁定。
//通过使用 lockedExt 字段,运行时系统可以追踪外部 LockOSThread 操作的状态,并确保在适当的时候将外部线程与 M 关联。
lockedExt uint32 // tracking for external LockOSThread
//用于跟踪内部 lockOSThread 操作的状态。
//lockedInt 字段用于跟踪 lockOSThread 操作的状态。
//当 lockedInt 的值为非零时,表示当前 M 的线程已被锁定到该 M;当 lockedInt 的值为零时,表示当前 M 的线程未被锁定。
//通过使用 lockedInt 字段,运行时系统可以追踪 lockOSThread 操作的状态,并确保在适当的时候将线程与 M 关联。
lockedInt uint32 // tracking for internal lockOSThread
//用于表示下一个正在等待锁的 M(Machine)。
//当一个 M 试图获取一个已被其他 M 锁定的资源时,它将被阻塞并加入到等待队列中。
//nextwaitm 字段用于维护等待队列中下一个等待锁的 M 的引用。
//通过使用 nextwaitm 字段,运行时系统可以维护 M 等待锁的顺序,并确保正确的调度和资源分配。
nextwaitm muintptr // next m waiting for lock
//waitunlockf 字段指向一个函数,该函数用于在 Goroutine 等待解锁时执行特定的操作。
//通过使用 waitunlockf 字段,运行时系统可以在 Goroutine 等待解锁时执行自定义的操作。
//这可能涉及到 Goroutine 的状态变更、唤醒等相关操作
waitunlockf func(*g, unsafe.Pointer) bool
//waitlock 字段用于在 Goroutine 等待解锁时存储与该等待操作相关的信息。
//它可以是一个锁对象、条件变量或其他用于同步的数据结构。
//通过使用 waitlock 字段,运行时系统可以在 Goroutine 等待解锁时将相关的等待状态和条件存储在 waitlock 字段中,
//以便在解锁时能够恢复相关的等待操作。
waitlock unsafe.Pointer
//用于表示在等待状态下的追踪事件类型。
//它可以用于跟踪和记录在 Goroutine 等待期间发生的事件,例如等待锁的持续时间、唤醒时机等
waittraceev byte
//waittraceskip 字段用于指定在追踪 Goroutine 等待状态时要跳过的堆栈帧数。
//它可以用于在追踪等待状态时忽略某些不需要的堆栈帧,以减少追踪的开销。
//通过使用 waittraceskip 字段,运行时系统可以根据需要设置要跳过的堆栈帧数,以适应不同的调试和分析需求。
waittraceskip int
//用于指示当前 M(Machine)是否处于启动追踪状态。
//startingtrace 字段用于跟踪 M 的启动过程中是否正在进行追踪。当 startingtrace 的值为 true 时,表示当前 M 正在进行启动追踪;
//通过使用 startingtrace 字段,运行时系统可以在 M 启动过程中追踪特定的事件或操作,以帮助调试和分析启动过程中的问题
startingtrace bool
//用于记录系统调用的计时器。syscalltick 字段用于跟踪 M(Machine)执行系统调用的次数。
//每当 M 执行一个系统调用时,syscalltick 字段的值会递增。
//通过使用 syscalltick 字段,运行时系统可以统计 M 执行系统调用的频率,并用于调度和性能分析。
syscalltick uint32
//用于将当前的 M(Machine)链接到 sched.freem 列表上。
//sched.freem 列表是调度器中的一个空闲 M 列表,用于存储处于空闲状态的 M。
//当一个 M 不再被使用时,它会被链接到 sched.freem 列表上,以便稍后可以被重新分配给其他的 Goroutine。
//通过使用 freelink 字段,运行时系统可以将当前的 M 链接到 sched.freem 列表上,以便将其标记为可用的空闲 M。
freelink *m // on sched.freem
//libcall用于在底层的 NOSPLIT 函数中存储太大而无法放置在栈上的数据,
//以避免将其放置在栈上造成栈溢出或栈帧过大的问题。
//通过使用 libcall 字段,运行时系统可以在需要的时候为底层的 NOSPLIT 函数分配额外的内存空间,以容纳较大的数据。
libcall libcall
//libcallpc 字段用于记录底层 NOSPLIT 函数中的 libcall 调用的程序计数器(PC)值。
//CPU 分析器使用这个字段来跟踪底层函数的执行情况,以便分析函数的性能和运行时间。
//通过使用 libcallpc 字段,运行时系统可以在底层 NOSPLIT 函数中跟踪和记录 CPU 分析信息,
//从而提供有关函数执行时间和性能的详细数据。
libcallpc uintptr // for cpu profiler
//libcallsp 字段用于记录底层 NOSPLIT 函数中的 libcall 调用的栈指针的位置。
//它可以帮助运行时系统正确地管理和恢复栈状态,以确保底层函数的正常执行。
//通过使用 libcallsp 字段,运行时系统可以在底层 NOSPLIT 函数中跟踪和管理栈的状态,以支持 libcall 的调用和返回。
libcallsp uintptr
//libcallg 字段用于记录底层 NOSPLIT 函数中的 libcall 调用所关联的 Goroutine。
//它可以帮助运行时系统跟踪和管理底层函数中涉及的 Goroutine,以确保正确的并发调度和协作。
//通过使用 libcallg 字段,运行时系统可以在底层 NOSPLIT 函数中关联正确的 Goroutine,
//以便正确处理 Goroutine 的上下文和状态。
libcallg guintptr
//用于在 Windows 系统上存储系统调用的参数。
//在 Windows 系统上,进行系统调用时需要将参数保存在 syscall 字段中。
syscall libcall // stores syscall parameters on windows
//用于在 VDSO 调用期间进行回溯时的栈指针(SP)位置。
//VDSO(Virtual Dynamic Shared Object)是一个虚拟共享对象,它由操作系统内核提供,
//用于提供一些常见的系统调用功能,以减少用户态和内核态之间的切换次数。
//在某些情况下,Golang 的运行时系统可以利用 VDSO 来加速一些系统调用的执行。
//vdsoSP 字段用于记录当前 Goroutine 在进行 VDSO 调用时的栈指针位置。
//它可以帮助运行时系统在进行 VDSO 调用期间进行正确的回溯,并获取与 VDSO 调用相关的堆栈跟踪信息
vdsoSP uintptr // SP for traceback while in VDSO call (0 if not in call)
//用于在 VDSO 调用期间进行回溯时的程序计数器(PC)位置。
//vdsoPC 字段用于记录当前 Goroutine 在进行 VDSO 调用时的程序计数器位置。
//跟vdsoSP一样,它可以帮助运行时系统在进行 VDSO 调用期间进行正确的回溯,并获取与 VDSO 调用相关的堆栈跟踪信息。
//后面我们会讲到handoff机制就需要vdsoSP和vdsoPC这两个寄存器来确保在G中断执行后能恢复之前的任务状态。当本线程 M 因为 G 进行的系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的 M’执行。当发生上线文切换时,需要对执行现场进行保护,以便下次被调度执行时进行现场恢复。Go 调度器 M 的栈保存在 G 对象上,只需要将 M 所需要的寄存器(SP、PC 等)保存到 G 对象上就可以实现现场保护。当这些寄存器数据被保护起来,就随时可以做上下文切换了,在中断之前把现场保存起来。如果此时 G 任务还没有执行完,M 可以将任务重新丢到 P 的任务队列,等待下 一次被调度执行。当再次被调度执行时,M 通过访问 G 的 vdsoSP、vdsoPC 寄存器进行现场恢复(从上次中断位置继续执行)
vdsoPC uintptr // PC for traceback while in VDSO call
// preemptGen 字段是一个 atomic.Uint32 类型的原子变量,用于计数已完成的抢占信号。
//preemptGen 字段用于记录已完成的抢占信号的数量。
//抢占信号是用于触发 Goroutine 抢占的机制,当一个 Goroutine 被抢占时,会增加 preemptGen 字段的计数。
//通过使用 preemptGen 字段,运行时系统可以检测到抢占请求是否成功。
//如果抢占请求失败,即 preemptGen 字段的计数没有增加,可以得知抢占操作未能生效。
preemptGen atomic.Uint32
// 用于表示当前 M(Machine)上是否存在挂起的抢占信号.
//当一个 Goroutine 请求抢占时,抢占信号会被设置为挂起状态,表示该 M 正在等待抢占。
//通过使用 signalPending 字段,运行时系统可以检查当前 M 是否存在挂起的抢占信号,以便在合适的时机触发抢占操作。
//需要注意的是,signalPending 字段是一个原子变量,用于在并发环境下进行安全的状态标记,以避免竞态条件。
//通过使用 signalPending 字段,Golang 的运行时系统可以准确地检测和处理抢占信号的挂起状态,以支持可靠的并发调度和资源管理。
signalPending atomic.Uint32
//dlogPerM 是一个布尔类型的常量,用于指示是否为每个 M(Machine)启用调试日志(debug log)。
//当 dlogPerM 为 true 时,每个 M 都会有自己独立的调试日志。这意味着每个 M 都会维护和输出其自己的调试日志信息。
//当 dlogPerM 为 false 时,所有的 M 共享同一个调试日志。这意味着所有 M 的调试日志信息会被写入到同一个日志中。
//需要注意的是,dlogPerM 是一个常量,用于在编译时确定调试日志的配置方式。
//它的设置通常是在运行时系统的构建配置中进行定义。
//通过使用 dlogPerM 常量,Golang 的运行时系统可以根据需要配置和管理调试日志,以帮助开发人员调试和排查问题。
dlogPerM
//表示操作系统相关的信息和状态。不同操作系统所存储的信息会有差异
mOS
//locksHeldLen 字段用于记录当前 M 持有的锁的数量,最多可以持有 10 个锁。
//该字段由锁排序代码(lock ranking code)维护和更新。
//通过使用 locksHeldLen 字段,运行时系统可以追踪和管理当前 M 持有的锁的数量,
//以便在锁的获取和释放过程中进行适当的调度和优化。
locksHeldLen int
// 用于存储当前 M(Machine)持有的锁的信息。
//locksHeld 字段是一个长度为 10 的数组,每个元素都是 heldLockInfo 类型的结构体,
//用于记录锁的详细信息,例如锁的地址、持有者 Goroutine 的信息等。
//通过使用 locksHeld 字段,运行时系统可以跟踪和管理当前 M 持有的锁的信息,以便进行锁的获取、释放和调度。
//锁的信息可以帮助运行时系统优化并发调度策略,避免死锁和竞态条件。
locksHeld [10]heldLockInfo
}
P 的底层结构#
// 取 processor 的首字母,为 M 的执行提供“上下文”,保存 M 执行 G 时的一些资源,
// 例如本地可运行 G 队列,memeory cache 等。
// 一个 M 只有绑定 P 才能执行 goroutine,
// 当 M 被阻塞时,整个 P 会被传递给其他 M ,或者说整个 P 被接管
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/...
//用于将 P(Processor)组织成链表。
//puintptr 实际上是一个 uintptr 类型的别名,它表示一个指向另一个 P 的指针。
//通过将多个 P 的 link 字段连接在一起,可以形成一个 P 链表。这种链表通常被用于调度和管理可用的处理器资源。
//在调度器中,当一个 Goroutine 需要被分配到一个 P 上执行时,调度器会遍历 P 链表,查找可用的 P,并将 Goroutine 分配给该 P。
//link 字段使得 P 可以在链表中进行连接和断开,以便于调度器根据需要动态地分配和回收处理器资源。
link puintptr
// 每次调用 schedule 时会加一.用于调度器的统计和性能分析.
//在 Golang 的调度器中,调度器会周期性地检查各个 P(Processor)的队列,并选择可执行的 Goroutine 进行调度。
//每次进行调度时,都会增加 schedtick 字段的值,以记录调度操作的次数。
schedtick uint32 // incremented on every scheduler call
// 每次系统调用时加一.
//Golang 的运行时系统可以跟踪和统计系统调用的次数,从而评估系统调用的开销和性能状况,并进行相应的优化和调整。
syscalltick uint32 // incremented on every system call
// 用于跟踪监视器观察到的时钟滴答。时钟滴答是一个在调度器中使用的时间单位,用于衡量 Goroutine 的执行时间和调度情况。
//监视器是 Golang 运行时系统中的一个组件,用于监控 Goroutine 的行为和性能,并进行相应的调整和优化。
//通过监视器,运行时系统可以检测出长时间运行的 Goroutine、阻塞的 Goroutine 等情况,并采取适当的措施来平衡调度和资源利用。
sysmontick sysmontick // last tick observed by sysmon
// 用于指向与当前 P(Processor)关联的 M(Machine).
//m 字段用于建立 P 和 M 之间的关联关系。每个 P 都会关联一个 M,表示该 P 当前正在执行的 M。
//当 P 需要执行 Goroutine 时,它会从与之关联的 M 中获取一个可执行的 Goroutine,并将其分配给自己执行。
//需要注意的是,如果 P 处于空闲状态(_Pidle),即没有可执行的 Goroutine,那么 m 字段将为空指针(nil)。
//通过使用 m 字段,P 和 M 实现了双向关联,使得运行时系统能够将 Goroutine 均匀地分配给各个处理器,以实现并行的执行。
m muintptr // back-link to associated m (nil if idle)
//mcache 是每个 M(Machine)与本地缓存相关的数据结构。它包含了与 M 关联的 Goroutine 执行时所需的本地分配缓存。
//每个 M 都有自己的本地缓存(mcache)来加速 Goroutine 的内存分配和回收。
//当 Goroutine 需要分配内存时,会首先检查关联的 M 的 mcache 是否为空。
//如果不为空,则直接从 mcache 中获取内存进行分配。这样可以避免频繁地向全局堆申请内存,提高分配速度。
//mcache 中还包含了分配和回收内存的一些其他信息,例如空闲对象列表、本地缓存的大小等。
mcache *mcache
//pcache 字段是一个 pageCache 类型的变量。它是一种无锁的缓存,用于分配内存页。
//页面缓存的作用是提供一块内存区域,供分配器在没有锁的情况下分配内存页。
//页面缓存是针对每个 P 的,每个 P 都有自己的页面缓存。
//当分配器需要分配内存页时,会首先查找页面缓存,检查其中是否有空闲的页面可用。
//如果有空闲页面,则直接分配给请求者,并更新位图表示页面状态。如果没有空闲页面,则需要从全局堆申请新的内存页。
//通过使用页面缓存,可以避免频繁地向操作系统申请内存页,提高内存分配和释放的效率。
//虽然 pcache 和 mcache 都与内存管理相关,但它们服务于不同的目的和使用场景。
//pcache 主要用于缓存页面,以减少频繁向操作系统申请内存页的开销,提高内存分配和释放的效率。
//它与 P 相关联,用于 P 的内存管理。
//mcache 则主要用于本地分配缓存,以加速 Goroutine 的内存分配和回收操作。它与 M 相关联,用于 M 的内存管理。
//在运行时系统中,P 和 M 是并发执行的基本单位,它们负责不同的任务和功能。
//pcache 和 mcache 是为了支持它们的不同需求而设计的
pcache pageCache
//raceprocctx 字段用于与竞态检测(race detection)相关的上下文信息。竞态检测是一种用于检测并发程序中数据竞争问题的工具。
//在启用竞态检测时,每个 Golang 程序都会关联一个竞态检测上下文,用于跟踪和记录竞态检测的相关信息。
//raceprocctx 字段存储了与竞态检测相关的上下文信息,通常是一个指向特定上下文数据结构的指针。
//竞态检测工具会在程序运行过程中检测并发访问共享数据的情况,通过比较访问的顺序和时间戳等信息来发现可能存在的数据竞争。
//当检测到数据竞争时,竞态检测器会使用竞态检测上下文来记录相关的信息,以便进行进一步的分析和报告。
raceprocctx uintptr
//deferpool 是一个 _defer 指针的切片,用于存储可用的延迟调用结构体
//deferpool 和 deferpoolbuf 用于管理 _defer 结构体的缓存池。
//deferpool也是一种池化技术,缓存池是为了避免频繁地分配和释放 _defer 结构体而设计的,以提高性能和效率。
//当函数执行过程中遇到延迟调用时,会从 deferpool 中获取一个可用的 _defer 结构体。
//如果 deferpool 中没有可用的结构体,则会动态分配一个新的 _defer 结构体。
//执行完延迟调用后,将 _defer 结构体放回 deferpool 中,以便重复使用。
//通过使用缓存池,可以减少动态内存分配的次数,从而提高程序的性能和效率。
deferpool []*_defer // pool of available defer structs (see panic.go)
//deferpoolbuf 是一个长度为 32 的 _defer 指针数组.
//当函数执行过程中遇到延迟调用时,会先尝试从 deferpoolbuf 中获取一个可用的 _defer 结构体。
//如果 deferpoolbuf 中有可用的结构体,则会使用该结构体来存储延迟调用的信息。
//如果 deferpoolbuf 中没有可用的结构体,则会动态分配一个新的 _defer 结构体来存储延迟调用的信息。
//通过使用 deferpoolbuf,可以减少动态内存分配的次数,提高延迟调用的效率和性能。
//deferpoolbuf与deferpool的区别?
//deferpoolbuf 是一个静态的数组,用于存储 _defer 结构体的缓冲区。
//它的大小是固定的,并且通常比较小,因此适用于频繁创建和释放延迟调用的场景。
//deferpool 是一个动态的切片,用于存储可用的 _defer 结构体。它的大小会根据需要动态调整,以适应不同的延迟调用的数量。
//它主要用于在运行时系统中管理和重用 _defer 结构体,以减少内存分配的开销。、
//即deferpoolbuf用于频繁创建和释放延迟调用的场景,而deferpool在于重用_defer
deferpoolbuf [32]*_defer
// 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
// 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
// gFree 是一个用于管理空闲 Goroutine 的数据结构。
//gFree 结构体包含以下字段:
//gList:一个双向链表,用于存储空闲的 Goroutine。每个空闲 Goroutine 在链表中都有一个节点,可以通过该节点访问链表中的其他空闲 Goroutine。
//n:一个 int32 类型的字段,表示空闲 Goroutine 的数量。
//当一个 Goroutine完成执行(状态为 Gdead)并且不再需要继续使用时,它会被添加到 gFree 的链表中,以便后续可以被重新利用。
//通过维护一个空闲 Goroutine 的链表,运行时系统可以节省创建和销毁 Goroutine 的开销,并且能够快速地获取可用的 Goroutine。
//当需要创建新的 Goroutine时,运行时系统会首先尝试从 gFree 链表中获取一个空闲 Goroutine。
//如果链表为空,那么会动态分配一个新的 Goroutine。
//当一个 Goroutine完成执行后,它会被释放并添加到 gFree 链表中,以便下次需要时可以重复使用。
//通过使用 gFree 数据结构,Golang 的运行时系统可以高效地管理和重用空闲的 Goroutine,
//从而提高程序的并发性能和资源利用率。
gFree struct {
gList
n int32
}
//sudogcache 和 sudogbuf 是用于管理和缓存 sudog 结构体的字段。
//sudog 是用于表示等待队列中的 Goroutine 的数据结构。
//sudogcache 是一个 []*sudog 切片,用于缓存可用的 sudog 结构体。
//这个缓存池用于避免频繁地分配和释放 sudog 结构体,以提高性能和效率。
//当一个 sudog 结构体不再使用时,会将其放回 sudogcache 中,以便下次需要时可以重复使用。
//sudogbuf 是一个长度为 128 的 *sudog 数组,用于存储 sudog 结构体的缓冲区。
//当需要创建新的 sudog 结构体时,首先会尝试从 sudogcache 中获取一个可用的结构体。
//如果缓存中有可用的结构体,则会使用它来存储 sudog 的信息。
//如果缓存中没有可用的结构体,则会动态分配一个新的 sudog 结构体。
//通过使用 sudogcache 和 sudogbuf,可以减少动态内存分配的次数,提高 sudog 结构体的创建和释放的效率。
sudogcache []*sudog
sudogbuf [128]*sudog
// mspancache 是用于缓存 mspan 对象的字段,它表示从堆中缓存的 mspan 对象。
//mspan 是用于管理内存分配的数据结构,每个 mspan 对象代表了一块内存页的管理信息。
//mspancache 结构体包含以下字段:
//len:一个整型字段,表示当前缓存中 mspan 对象的数量。
//buf:一个长度为 128 的 mspan 指针数组,用于存储 mspan 对象。
//mspancache 用于缓存 mspan 对象,以提高内存分配的性能。
//当需要分配新的内存页时,运行时系统首先会尝试从 mspancache 中获取一个可用的 mspan 对象。
//如果缓存中有可用的对象,就会将其分配给新的内存页。这样可以减少对堆的访问,提高内存分配的效率。
//需要注意的是,mspancache 的长度(len 字段)是显式声明的,
//因为该字段在一些分配代码路径中可能涉及到不能使用写屏障的情况。因此,需要通过显式管理长度来避免写屏障的使用。
//通过使用 mspancache,Golang 的运行时系统可以高效地管理和重用 mspan 对象,以提高内存分配和管理的性能。
mspancache struct {
// 一个整型字段,表示当前缓存中 mspan 对象的数量。
len int
//一个长度为 128 的 mspan 指针数组,用于存储 mspan 对象。
buf [128]*mspan
}
//用于存储追踪信息的缓冲区
tracebuf traceBufPtr
// 用于指示是否需要跟踪垃圾回收器的扫描事件。
//当 traceSweep 为 true 时,垃圾回收器会记录和追踪扫描事件。
//这样可以提供详细的垃圾回收器执行信息,用于性能分析和调试。
//具体来说,当需要进行垃圾回收时,如果 traceSweep 为 true,则会延迟触发扫描开始事件,
//直到至少一个内存范围(span)被完全扫描完成。
//这样可以确保垃圾回收器的扫描事件只包括实际进行扫描的内存范围。
traceSweep bool
// traceSwept 和 traceReclaimed 是用于跟踪垃圾回收器在当前扫描循环中扫描和回收的字节数的字段。
//traceSwept 是一个 uintptr 类型的字段,用于跟踪当前扫描循环中已经扫描的字节数。它表示垃圾回收器已经检查并标记为可回收的内存字节数。
//
//traceReclaimed 是一个 uintptr 类型的字段,用于跟踪当前扫描循环中已经回收的字节数。
//它表示垃圾回收器已经成功回收的内存字节数。
//这两个字段的目的是提供有关垃圾回收器执行的详细信息,以便进行性能分析和调试。
//通过跟踪已扫描和回收的字节数,可以评估垃圾回收器的效率和性能。
traceSwept, traceReclaimed uintptr
//palloc 是一个 persistentAlloc 类型的字段,用于每个 P(处理器)维护持久性分配(persistent allocation)以避免使用互斥锁。
//persistentAlloc 是一种分配器,用于在特定的处理器上分配固定大小的内存块,而无需使用互斥锁进行同步。
//它被设计为在每个 P 上独立运行,以避免并发访问的竞争条件。
//通过在每个 P 上维护一个独立的 persistentAlloc 实例,Golang 运行时系统可以提高并发性能和分配的效率。
//每个 P 都拥有自己的 persistentAlloc 实例,因此可以独立地进行分配操作,而无需在多个 P 之间进行同步。
palloc persistentAlloc // per-P to avoid mutex
// timer0When 是一个 atomic.Int64 类型的字段,用于表示定时器堆中第一个条目的触发时间。
//定时器堆是一种数据结构,用于管理和调度定时器的触发时间。
//每个定时器都会被插入到定时器堆中,并根据其触发时间进行排序。
//timer0When 字段用于表示定时器堆中第一个条目的触发时间。
//timer0When 字段是一个原子类型的整数,用于确保并发访问时的安全性。
//它存储了定时器堆中第一个条目的触发时间,以纳秒为单位表示。如果定时器堆为空,timer0When 字段的值将为 0。
//通过使用原子操作保护 timer0When 字段,运行时系统可以确保多个线程或 Goroutine 并发地访问和更新定时器堆的触发时间,
//而不会发生竞争条件或数据不一致的情况。
timer0When atomic.Int64
// timerModifiedEarliest 是一个 atomic.Int64 类型的字段,
//用于表示具有 "timerModifiedEarlier" 状态的定时器中最早的下一个触发时间。
timerModifiedEarliest atomic.Int64
// 用于表示在辅助分配(assistAlloc)中花费的纳秒数。辅助分配是垃圾回收期间由 P 执行的帮助分配操作。
gcAssistTime int64 // Nanoseconds in assistAlloc
//用于在分数化标记工作器(fractional mark worker)中花费的纳秒数。分数化标记工作器是垃圾回收期间执行部分标记的工作器。
gcFractionalMarkTime int64 // Nanoseconds in fractional mark worker (atomic)
//用于跟踪垃圾回收器 CPU 限制器的事件的字段
// 在 Golang 的运行时系统中,垃圾回收器 CPU 限制器用于限制垃圾回收期间消耗的 CPU 时间,
//以避免垃圾回收对应用程序的执行造成过大的影响。
//limiterEvent 包含一个 stamp 字段,它是一个 atomic.Uint64 类型的变量。
//stamp 字段用于存储一个 limiterEventStamp 值,该值表示事件的时间戳。
//通过使用 limiterEvent,运行时系统可以跟踪和记录特定事件在时间上的发生情况。
//这对于分析和优化垃圾回收过程中的 CPU 时间消耗非常有用。
//通过收集事件的时间戳,可以计算出垃圾回收期间 CPU 时间的分布和利用率,从而帮助开发人员进行性能分析和调优
limiterEvent limiterEvent
// 表示下一个标记工作器(mark worker)运行的模式。
//它用于与通过 gcController.findRunnableGCWorker 选择的工作器 Goroutine 进行通信。
//在调度其他 Goroutine 时,必须将此字段设置为 gcMarkWorkerNotWorker。
gcMarkWorkerMode gcMarkWorkerMode
// 最近一个标记工作器开始运行的时间,以纳秒为单位。
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.
// P 的 GC工作的缓冲区缓存。
// GC工作的缓冲区由写屏障填充、由助理分配器(mutator assists)消耗,并在某些 GC 状态转换时释放。
gcw gcWork
// P 的 GC 写屏障缓存,后续版本可能会考虑缓存正在运行的G。
wbBuf wbBuf
// 如果为 1,表示在下一个安全点运行 sched.safePointFn 函数。
//在 Golang 的运行时系统中,安全点是程序执行时的一种特殊状态,可以在此状态下进行垃圾回收和其他系统任务。
//运行时系统会在安全点中暂停所有 Goroutine 的执行,并在安全点之间执行一些必要的操作,
//例如垃圾回收的扫描和标记,调度器或其他系统任务相关的功能。
runSafePointFn uint32 // if 1, run sched.safePointFn at next safe point
//statsSeq是一个计数器,指示当前的 P 是否正在写入任何统计信息。
//其值为奇数时表示正在写入统计信息时
statsSeq atomic.Uint32
//timersLock 是用于保护计时器(timers)的互斥锁(mutex)。
//互斥锁是一种同步原语,它提供了对共享资源的独占访问权。通过在对计时器进行访问之前获取互斥锁,并在访问完成后释放锁,
//可以确保同一时间只有一个 Goroutine 能够访问计时器,避免并发访问导致的数据竞争和不一致性。
//在正常情况下,运行时系统会在运行在同一 P 上的 Goroutine 中访问计时器,但调度器也可能会在不同的 P 上访问计时器。
timersLock mutex
// timers 是一个用于存储定时器的数组,表示在某个特定时间要执行的操作。该字段用于实现标准库的 time 包。
//访问 timers 字段时必须持有 timersLock 互斥锁,避免并发操作引起的竞态条件。
timers []*timer
// 表示 P(Processor) 的堆(heap)中的定时器数量。
numTimers atomic.Uint32
// 表示 P(Processor) 的堆(heap)中被删除的定时器数量
deletedTimers atomic.Uint32
// timerRaceCtx 字段用于在执行定时器函数时记录竞争上下文。
//在并发环境下,多个 Goroutine 可能会同时访问和执行定时器函数,
//因此需要使用竞争上下文来追踪和标识定时器函数的执行情况。
timerRaceCtx uintptr
//用于累积活跃 Goroutine(即可进行堆栈扫描的 Goroutine)所占用的堆栈空间大小。
//在 Golang 的运行时系统中,垃圾回收器(GC)负责回收不再使用的内存。
//堆栈扫描是垃圾回收的一部分,它用于识别堆栈上的对象并进行标记,以确保不会回收仍然被引用的对象。
//maxStackScanDelta 字段用于跟踪并累积活跃 Goroutine 所占用的堆栈空间大小的变化。
//当堆栈空间的变化达到一定阈值(maxStackScanSlack 或 -maxStackScanSlack),
//maxStackScanDelta 字段的值会被刷新到 gcController.maxStackScan 字段中。
//通过记录 maxStackScanDelta,运行时系统可以实时跟踪堆栈空间的使用情况, 并在达到阈值时触发相应的处理逻辑。
//这有助于优化垃圾回收器的性能和效率,以及控制堆栈扫描的成本。
maxStackScanDelta int64
// scannedStackSize 和 scannedStacks 是用于记录与 GC 时间相关的有关当前 Goroutine 的统计信息的字段。
//scannedStackSize 字段用于累积当前 P(Processor) 扫描的 Goroutine 堆栈的大小。
//它表示在 GC 过程中扫描的 Goroutine 堆栈实际使用的空间大小(hi - sp)。
//scannedStacks 字段用于累积当前 P 扫描的 Goroutine 数量。
//这些字段的目的是跟踪当前 P 在 GC 过程中扫描的 Goroutine 的堆栈信息。
//通过收集和记录这些统计数据,运行时系统可以评估 GC 过程中 Goroutine 堆栈的使用情况,
//以提供对垃圾回收过程中 Goroutine 堆栈的细粒度监控和优化支持。
scannedStackSize uint64 // stack size of goroutines scanned by this P
scannedStacks uint64 // number of goroutines scanned by this P
// 用于指示该 P(Processor)是否应尽快进入调度器(scheduler),而不考虑当前运行在该 P 上的 Goroutine。
//在 Golang 的运行时系统中,调度器负责协调 Goroutine 的执行。
//为了实现公平的调度和避免 Goroutine 长时间占用 P,调度器会周期性地检查是否需要进行调度切换,即让其他 Goroutine 获取执行机会。
//preempt 字段用于标记该 P 是否应立即进入调度器。
//当 preempt 字段为 true 时,该 P 将优先进入调度器,即使当前正在运行的 Goroutine 尚未完成。
//这可以有效地实现调度器的抢占式调度,以避免某个 Goroutine 长时间占用 P,导致其他 Goroutine 饥饿。
//通过设置 preempt 字段,运行时系统可以实现对长时间运行的 Goroutine 的抢占,
//确保其他 Goroutine 有机会获取执行时间,提高整体系统的公平性和性能。
preempt bool
// pageTraceBuf 是一个用于记录页面分配、释放和清理追踪的缓冲区。
//在 Golang 的运行时系统中,可以启用 GOEXPERIMENT=pagetrace 实验特性来收集与页面管理相关的追踪信息。
//当启用了 pagetrace 实验特性时,运行时系统会追踪页面的分配、释放和清理操作,并将相关的追踪信息记录下来。
//pageTraceBuf 是一个用于缓存页面追踪信息的缓冲区。它会在需要记录页面追踪信息时被使用。
//当收集到足够的追踪信息后,运行时系统会将其写入输出流或日志文件中,以供进一步分析和调试。
//需要注意的是,pageTraceBuf 字段仅在启用了 pagetrace 实验特性时才会被使用。
//它是运行时系统内部的一个工具,用于收集调试和性能分析所需的页面追踪信息。
//通过使用 pageTraceBuf 字段,Golang 的运行时系统能够提供额外的工具和功能,以帮助开发人员诊断和优化与页面管理相关的问题。
//它提供了对页面分配、释放和清理的细粒度追踪,有助于理解和分析运行时系统的内存管理行为。
pageTraceBuf pageTraceBuf
//pad cpu.CacheLinePad //注意:该字段在当前版本已被移除
// p 结构体中的填充字段(pad)不再需要。填充字段在以前的版本中用于解决伪共享(False sharing)问题,
//即在多个处理器上访问同一缓存行而导致的性能下降。
//然而,在当前的实现中,p 结构体的大小已经足够大,以至于它的大小类是缓存行大小的整数倍(对于我们的任何架构都是如此)。
//这意味着 p 结构体的大小已经足够大,不会与其他结构体或变量共享同一缓存行。
//因此,填充字段不再需要来解决伪共享问题。这样一来,可以减少额外的填充字段,从而节省内存空间。
//这个改变对于解决伪共享问题有一定的意义,因为它减少了不必要的内存开销,并提高了 p 结构体的紧凑性。
//这使得每个 p 结构体在处理器之间的迁移和访问时更加高效,同时减少了缓存行的冲突,进一步提升了并发性能。
}
select 实现代码#
// selectgo函数实现了Go语言中的select语句。
//遍历所有的case语句,如果所有的case都未就绪,则走default,如果没有default,则会阻塞。
//如果有就绪channel,则直接跳出顺序进行管道操作并返回
// 第一个返回值返回的是被选择的scase的索引,这个索引就是需要执行的case分支;
// 第一个返回值标记这个case语句是否接收到了一个值。
func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
if debugSelect {
print("select: cas0=", cas0, "\n")
}
// NOTE: In order to maintain a lean stack size, the number of scases
// is capped at 65536.
cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
ncases := nsends + nrecvs
scases := cas1[:ncases:ncases]
pollorder := order1[:ncases:ncases]
lockorder := order1[ncases:][:ncases:ncases]
// NOTE: pollorder/lockorder's underlying array was not zero-initialized by compiler.
// Even when raceenabled is true, there might be select
// statements in packages compiled without -race (e.g.,
// ensureSigM in runtime/signal_unix.go).
var pcs []uintptr
if raceenabled && pc0 != nil {
pc1 := (*[1 << 16]uintptr)(unsafe.Pointer(pc0))
pcs = pc1[:ncases:ncases]
}
casePC := func(casi int) uintptr {
if pcs == nil {
return 0
}
return pcs[casi]
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
// The compiler rewrites selects that statically have
// only 0 or 1 cases plus default into simpler constructs.
// The only way we can end up with such small sel.ncase
// values here is for a larger select in which most channels
// have been nilled out. The general code handles those
// cases correctly, and they are rare enough not to bother
// optimizing (and needing to test).
// generate permuted order
norder := 0
for i := range scases {
cas := &scases[i]
// Omit cases without channels from the poll and lock orders.
if cas.c == nil {
cas.elem = nil // allow GC
continue
}
j := fastrandn(uint32(norder + 1))
pollorder[norder] = pollorder[j]
pollorder[j] = uint16(i)
norder++
}
pollorder = pollorder[:norder]
lockorder = lockorder[:norder]
// sort the cases by Hchan address to get the locking order.
// simple heap sort, to guarantee n log n time and constant stack footprint.
for i := range lockorder {
j := i
// Start with the pollorder to permute cases on the same channel.
c := scases[pollorder[i]].c
for j > 0 && scases[lockorder[(j-1)/2]].c.sortkey() < c.sortkey() {
k := (j - 1) / 2
lockorder[j] = lockorder[k]
j = k
}
lockorder[j] = pollorder[i]
}
for i := len(lockorder) - 1; i >= 0; i-- {
o := lockorder[i]
c := scases[o].c
lockorder[i] = lockorder[0]
j := 0
for {
k := j*2 + 1
if k >= i {
break
}
if k+1 < i && scases[lockorder[k]].c.sortkey() < scases[lockorder[k+1]].c.sortkey() {
k++
}
if c.sortkey() < scases[lockorder[k]].c.sortkey() {
lockorder[j] = lockorder[k]
j = k
continue
}
break
}
lockorder[j] = o
}
if debugSelect {
for i := 0; i+1 < len(lockorder); i++ {
if scases[lockorder[i]].c.sortkey() > scases[lockorder[i+1]].c.sortkey() {
print("i=", i, " x=", lockorder[i], " y=", lockorder[i+1], "\n")
throw("select: broken sort")
}
}
}
// lock all the channels involved in the select
sellock(scases, lockorder)
var (
gp *g
sg *sudog
c *hchan
k *scase
sglist *sudog
sgnext *sudog
qp unsafe.Pointer
nextp **sudog
)
// pass 1 - look for something already waiting
var casi int
var cas *scase
var caseSuccess bool
var caseReleaseTime int64 = -1
var recvOK bool
for _, casei := range pollorder {
casi = int(casei)
cas = &scases[casi]
c = cas.c
if casi >= nsends {
sg = c.sendq.dequeue()
if sg != nil {
goto recv
}
if c.qcount > 0 {
goto bufrecv
}
if c.closed != 0 {
goto rclose
}
} else {
if raceenabled {
racereadpc(c.raceaddr(), casePC(casi), chansendpc)
}
if c.closed != 0 {
goto sclose
}
sg = c.recvq.dequeue()
if sg != nil {
goto send
}
if c.qcount < c.dataqsiz {
goto bufsend
}
}
}
if !block {
selunlock(scases, lockorder)
casi = -1
goto retc
}
// pass 2 - enqueue on all chans
gp = getg()
if gp.waiting != nil {
throw("gp.waiting != nil")
}
nextp = &gp.waiting
for _, casei := range lockorder {
casi = int(casei)
cas = &scases[casi]
c = cas.c
sg := acquireSudog()
sg.g = gp
sg.isSelect = true
// No stack splits between assigning elem and enqueuing
// sg on gp.waiting where copystack can find it.
sg.elem = cas.elem
sg.releasetime = 0
if t0 != 0 {
sg.releasetime = -1
}
sg.c = c
// Construct waiting list in lock order.
*nextp = sg
nextp = &sg.waitlink
if casi < nsends {
c.sendq.enqueue(sg)
} else {
c.recvq.enqueue(sg)
}
}
// wait for someone to wake us up
gp.param = nil
// Signal to anyone trying to shrink our stack that we're about
// to park on a channel. The window between when this G's status
// changes and when we set gp.activeStackChans is not safe for
// stack shrinking.
gp.parkingOnChan.Store(true)
gopark(selparkcommit, nil, waitReasonSelect, traceBlockSelect, 1)
gp.activeStackChans = false
sellock(scases, lockorder)
gp.selectDone.Store(0)
sg = (*sudog)(gp.param)
gp.param = nil
// pass 3 - dequeue from unsuccessful chans
// otherwise they stack up on quiet channels
// record the successful case, if any.
// We singly-linked up the SudoGs in lock order.
casi = -1
cas = nil
caseSuccess = false
sglist = gp.waiting
// Clear all elem before unlinking from gp.waiting.
for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink {
sg1.isSelect = false
sg1.elem = nil
sg1.c = nil
}
gp.waiting = nil
for _, casei := range lockorder {
k = &scases[casei]
if sg == sglist {
// sg has already been dequeued by the G that woke us up.
casi = int(casei)
cas = k
caseSuccess = sglist.success
if sglist.releasetime > 0 {
caseReleaseTime = sglist.releasetime
}
} else {
c = k.c
if int(casei) < nsends {
c.sendq.dequeueSudoG(sglist)
} else {
c.recvq.dequeueSudoG(sglist)
}
}
sgnext = sglist.waitlink
sglist.waitlink = nil
releaseSudog(sglist)
sglist = sgnext
}
if cas == nil {
throw("selectgo: bad wakeup")
}
c = cas.c
if debugSelect {
print("wait-return: cas0=", cas0, " c=", c, " cas=", cas, " send=", casi < nsends, "\n")
}
if casi < nsends {
if !caseSuccess {
goto sclose
}
} else {
recvOK = caseSuccess
}
if raceenabled {
if casi < nsends {
raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
} else if cas.elem != nil {
raceWriteObjectPC(c.elemtype, cas.elem, casePC(casi), chanrecvpc)
}
}
if msanenabled {
if casi < nsends {
msanread(cas.elem, c.elemtype.Size_)
} else if cas.elem != nil {
msanwrite(cas.elem, c.elemtype.Size_)
}
}
if asanenabled {
if casi < nsends {
asanread(cas.elem, c.elemtype.Size_)
} else if cas.elem != nil {
asanwrite(cas.elem, c.elemtype.Size_)
}
}
selunlock(scases, lockorder)
goto retc
bufrecv:
// can receive from buffer
if raceenabled {
if cas.elem != nil {
raceWriteObjectPC(c.elemtype, cas.elem, casePC(casi), chanrecvpc)
}
racenotify(c, c.recvx, nil)
}
if msanenabled && cas.elem != nil {
msanwrite(cas.elem, c.elemtype.Size_)
}
if asanenabled && cas.elem != nil {
asanwrite(cas.elem, c.elemtype.Size_)
}
recvOK = true
qp = chanbuf(c, c.recvx)
if cas.elem != nil {
typedmemmove(c.elemtype, cas.elem, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
selunlock(scases, lockorder)
goto retc
bufsend:
// can send to buffer
if raceenabled {
racenotify(c, c.sendx, nil)
raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
}
if msanenabled {
msanread(cas.elem, c.elemtype.Size_)
}
if asanenabled {
asanread(cas.elem, c.elemtype.Size_)
}
typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem)
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
selunlock(scases, lockorder)
goto retc
recv:
// can receive from sleeping sender (sg)
recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
if debugSelect {
print("syncrecv: cas0=", cas0, " c=", c, "\n")
}
recvOK = true
goto retc
rclose:
// read at end of closed channel
selunlock(scases, lockorder)
recvOK = false
if cas.elem != nil {
typedmemclr(c.elemtype, cas.elem)
}
if raceenabled {
raceacquire(c.raceaddr())
}
goto retc
send:
// can send to a sleeping receiver (sg)
if raceenabled {
raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
}
if msanenabled {
msanread(cas.elem, c.elemtype.Size_)
}
if asanenabled {
asanread(cas.elem, c.elemtype.Size_)
}
send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
if debugSelect {
print("syncsend: cas0=", cas0, " c=", c, "\n")
}
goto retc
retc:
if caseReleaseTime > 0 {
blockevent(caseReleaseTime-t0, 1)
}
return casi, recvOK
sclose:
// send on closed channel
selunlock(scases, lockorder)
panic(plainError("send on closed channel"))
}
func (c *hchan) sortkey() uintptr {
return uintptr(unsafe.Pointer(c))
}
// A runtimeSelect is a single case passed to rselect.
// This must match ../reflect/value.go:/runtimeSelect
type runtimeSelect struct {
dir selectDir
typ unsafe.Pointer // channel type (not used here)
ch *hchan // channel
val unsafe.Pointer // ptr to data (SendDir) or ptr to receive buffer (RecvDir)
}
// These values must match ../reflect/value.go:/SelectDir.
type selectDir int
const (
_ selectDir = iota
selectSend // case Chan <- Send
selectRecv // case <-Chan:
selectDefault // default
)
//go:linkname reflect_rselect reflect.rselect
func reflect_rselect(cases []runtimeSelect) (int, bool) {
if len(cases) == 0 {
block()
}
sel := make([]scase, len(cases))
orig := make([]int, len(cases))
nsends, nrecvs := 0, 0
dflt := -1
for i, rc := range cases {
var j int
switch rc.dir {
case selectDefault:
dflt = i
continue
case selectSend:
j = nsends
nsends++
case selectRecv:
nrecvs++
j = len(cases) - nrecvs
}
sel[j] = scase{c: rc.ch, elem: rc.val}
orig[j] = i
}
// Only a default case.
if nsends+nrecvs == 0 {
return dflt, false
}
// Compact sel and orig if necessary.
if nsends+nrecvs < len(cases) {
copy(sel[nsends:], sel[len(cases)-nrecvs:])
copy(orig[nsends:], orig[len(cases)-nrecvs:])
}
order := make([]uint16, 2*(nsends+nrecvs))
var pc0 *uintptr
if raceenabled {
pcs := make([]uintptr, nsends+nrecvs)
for i := range pcs {
selectsetpc(&pcs[i])
}
pc0 = &pcs[0]
}
chosen, recvOK := selectgo(&sel[0], &order[0], pc0, nsends, nrecvs, dflt == -1)
// Translate chosen back to caller's ordering.
if chosen < 0 {
chosen = dflt
} else {
chosen = orig[chosen]
}
return chosen, recvOK
}
func (q *waitq) dequeueSudoG(sgp *sudog) {
x := sgp.prev
y := sgp.next
if x != nil {
if y != nil {
// middle of queue
x.next = y
y.prev = x
sgp.next = nil
sgp.prev = nil
return
}
// end of queue
x.next = nil
q.last = x
sgp.prev = nil
return
}
if y != nil {
// start of queue
y.prev = nil
q.first = y
sgp.next = nil
return
}
// x==y==nil. Either sgp is the only element in the queue,
// or it has already been removed. Use q.first to disambiguate.
if q.first == sgp {
q.first = nil
q.last = nil
}
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
推荐文章: