"explanation":"P(Processor)是逻辑处理器,不是物理 CPU 核心。P 持有一个本地 goroutine 运行队列(local run queue)和执行 goroutine 所需的资源上下文(如 mcache、span 等)。G 是 goroutine,M 是操作系统线程。默认情况下 P 的数量等于 GOMAXPROCS,默认值为 CPU 核心数。P 在 G、M 之间起桥梁作用,G 绑定到 P 的本地队列上运行,M 则需要绑定一个 P 才能执行 G。",
"source":null,
"related":[]
},
{
"id":"sc-002",
"type":"single_choice",
"difficulty":3,
"tags":[
"Go",
"GMP",
"work-stealing"
],
"question":"关于 Go GMP 调度器的 work-stealing 机制,以下描述正确的是?",
"options":{
"A":"当某个 P 的本地队列为空时,它会从其他 P 的本地队列中窃取一半的 goroutine",
"B":"当某个 M 空闲时,它会随机选择一个 P 并抢占其正在运行的 goroutine",
"C":"work-stealing 仅在全局队列为空时才会触发",
"D":"每次窃取操作都会从目标 P 的队列头部取走一个 goroutine"
},
"answer":"A",
"explanation":"当一个 P 的本地队列为空且全局队列也为空时,调度器会执行 work-stealing:随机选择另一个 P,将其本地队列中的 goroutine 偷走一半。这种设计保证了负载均衡——不会出现某个 P 忙碌而另一个 P 空闲的情况。选项 C 错误,work-stealing 的前提是本地队列为空,然后先尝试全局队列,再尝试 work-stealing。选项 D 错误,窃取时是取走一半(约 half),而非单个。",
"source":null,
"related":[]
},
{
"id":"sc-003",
"type":"single_choice",
"difficulty":4,
"tags":[
"Go",
"GMP",
"sysmon",
"netpoller"
],
"question":"Go 运行时中 sysmon 线程的主要职责不包括以下哪项?",
"options":{
"A":"检测并抢占长时间运行的 goroutine",
"B":"回收超过 10ms 未使用的内存堆栈",
"C":"将因系统调用阻塞的 M 上的 P 抢占并绑定到空闲 M",
"D":"编译 Go 源码中的泛型函数"
},
"answer":"D",
"explanation":"sysmon 是一个特殊的后台 M(不绑定 P),其职责包括:1)抢占运行超过 10ms 的 goroutine(基于协作+信号的抢占式调度);2)将阻塞在系统调用上的 M 的 P 抢占出来绑定到空闲 M 上,保证 P 不被浪费;3)触发 GC 的后台标记;4)回收长时间未使用的堆栈。编译工作由编译器在编译期完成,与 sysmon 无关。netpoller 也与 sysmon 有协作关系,用于处理网络 I/O 的轮询。",
"source":null,
"related":[]
},
{
"id":"sc-004",
"type":"single_choice",
"difficulty":2,
"tags":[
"Go",
"channel",
"hchan"
],
"question":"在 Go 中,一个无缓冲 channel(unbuffered channel)的发送操作何时完成?",
"options":{
"A":"将值放入 channel 缓冲区后立即返回",
"B":"必须有另一个 goroutine 同时执行接收操作,两者同步后才完成",
"C":"无论是否有接收方,发送操作都会立即返回",
"D":"发送操作会阻塞直到 channel 被关闭"
},
"answer":"B",
"explanation":"无缓冲 channel(make(chan T))是同步通道,发送方在接收方准备好之前会阻塞。只有当另一个 goroutine 执行了接收操作,发送和接收才会同步完成。这就是 Go 的'通信通过共享内存'而非'共享内存通过通信'的核心体现。有缓冲 channel(make(chan T, n))在缓冲区未满时发送不会阻塞,这是两者的关键区别。",
"explanation":"Go 的 race detector 基于 Google 的 ThreadSanitizer(TSan)技术,在编译期插桩(instrumentation)并在运行时动态追踪所有内存访问和同步操作,构建 happens-before 关系图。如果两个并发访问(至少一个是写)之间没有 happens-before 关系,就报告数据竞争。选项 A 错误,它不是纯静态分析,需要运行时执行才能检测。选项 B 错误,ASan 检测的是内存安全(越界、use-after-free),不是数据竞争。选项 D 完全不正确。",
"explanation":"JVM 对 synchronized 的优化采用逐步升级策略:1)无锁状态 → 首次进入同步块时,如果对象头 Mark Word 未偏向任何线程,升级为偏向锁(仅记录线程 ID,无 CAS 开销);2)当第二个线程尝试竞争时,偏向锁撤销,升级为轻量级锁(通过 CAS 将 Mark Word 替换为指向栈帧中 Lock Record 的指针);3)轻量级锁竞争失败(自旋超过一定次数),膨胀为重量级锁(OS mutex,未获锁的线程阻塞挂起)。注意:偏向锁在 JDK 15 后默认关闭,因为其撤销成本较高。",
"question":"在 Go 运行时中,一个 goroutine 正在执行系统调用(如文件 I/O),此时该 G 的状态最可能被标记为以下哪一个?",
"options":{
"A":"_Grunnable",
"B":"_Grunning",
"C":"_Gsyscall",
"D":"_Gwaiting"
},
"answer":"C",
"explanation":"Go 运行时为 goroutine 定义了多个状态,其中 _Gsyscall 表示 goroutine 正在执行系统调用。当 G 进入阻塞式系统调用时,调度器会将其状态置为 _Gsyscall,同时将该 G 与当前 M 解绑,并将 P 转移到另一个空闲 M 上继续执行其他 G,以避免宝贵的 P 资源被浪费。_Grunnable 表示 G 可运行但尚未被调度执行,通常在运行队列中等待。_Grunning 表示 G 正在某个 M 上实际执行。_Gwaiting 表示 G 被阻塞等待某个事件(如 channel 操作、锁、网络 I/O 等),但不包括系统调用场景。因此系统调用期间的状态为 _Gsyscall,这是 GMP 模型中保证系统调用不会阻塞整个 P 的关键机制。"
"explanation":"M 代表操作系统线程(Machine),P 代表逻辑处理器(Processor)。M 可以处于未与 P 绑定的状态,此时 M 没有可执行的 G,会进入休眠等待被唤醒。当某个 G 发生系统调用阻塞时,调度器会将 P 从该 M 上剥离并绑定到另一个空闲或新创建的 M 上,原来的 M 则在系统调用完成后尝试重新获取 P。选项 A 错误,因为 M 和 P 的数量可以不同,M 是按需创建的。选项 C 错误,P 的数量默认等于 CPU 核心数,但可以通过 runtime.GOMAXPROCS() 动态修改。选项 D 错误,M 的数量不受 P 的限制——当大量 G 发生系统调用时,可能创建远多于 P 的 M,每个阻塞的系统调用都可能需要一个额外的 M 来保持 P 的利用率。"
"explanation":"Go 1.14 引入的基于信号的异步抢占使用 SIGURG 信号(而非 SIGINT),解决了协作式抢占的根本缺陷。在 Go 1.14 之前,抢占检查点仅在函数调用时插入,这意味着一个没有任何函数调用的紧密 for 循环(如 `for {}`)会永远占用 P,导致其他 goroutine 无法运行、GC 无法 STW。异步抢占通过向目标 M 发送 SIGURG 信号,使其在信号处理函数中保存 G 的上下文并切换到调度器,从而实现了对任何 goroutine 的抢占,无论其是否在函数调用点。选项 A 错误,使用的是 SIGURG 而非 SIGINT。选项 C 错误,异步抢占并未完全取代协作式抢占——函数调用前的抢占检查点依然存在,异步抢占是协作式的补充。选项 D 错误,异步抢占完全由运行时自动执行,无需程序员介入。"
},
{
"id":"sc-021",
"type":"single_choice",
"difficulty":5,
"tags":[
"Go",
"GMP"
],
"question":"当一个 P 执行 runtime.schedule() 尝试获取下一个可运行的 G 时,调度器的优先级顺序是以下哪种?",
"options":{
"A":"全局队列 → 本地队列 → netpoll → 其他 P 的队列(work-stealing)",
"B":"本地队列 → 全局队列 → 其他 P 的队列(work-stealing)→ netpoll",
"C":"netpoll → 本地队列 → 全局队列 → 其他 P 的队列(work-stealing)",
"D":"本地队列 → 其他 P 的队列(work-stealing)→ 全局队列 → netpoll"
},
"answer":"B",
"explanation":"runtime.schedule() 的调度逻辑遵循以下优先级:(1) 首先检查当前 P 的本地运行队列(local run queue),这是最快的路径,无锁访问,cache 友好。(2) 每隔 61 次调度(通过 schedtick % 61 == 0 判断),或本地队列为空时,检查全局运行队列(global run queue),防止全局队列中的 G 饥饿。(3) 尝试从 netpoll 获取就绪的 G。(4) 尝试通过 work-stealing 从其他 P 的本地队列中窃取一半的 G。如果以上所有途径都没有找到可运行的 G,当前 M 会与 P 解绑,进入休眠状态直到有新的 G 可运行。选项 A 将全局队列放在最高优先级,这会因锁竞争严重影响性能。选项 C 将 netpoll 放在最高位不准确,netpoll 检查有频率控制。选项 D 完全省略了 netpoll 步骤,且将全局队列检查放在最后,会导致全局队列中的 G 长期得不到执行。"
},
{
"id":"sc-022",
"type":"single_choice",
"difficulty":3,
"tags":[
"Go",
"GMP"
],
"question":"当一个 P 的本地运行队列为空时,它会尝试通过 work-stealing 从其他 P 窃取 G。关于窃取目标的选择,以下描述正确的是?",
"options":{
"A":"总是选择本地队列最长的那个 P 进行窃取",
"B":"按照 P 的编号顺序,从第一个 P 开始依次尝试",
"C":"随机选择一个 P 作为窃取目标,窃取其本地队列中一半的 G",
"D":"将所有其他 P 的队列合并后取平均数量的 G"
},
"answer":"C",
"explanation":"Go 运行时的 work-stealing 算法在选择窃取目标时采用随机策略。当 P 发现自己的本地队列为空(且全局队列和 netpoll 也没有可用 G)时,它会生成一个随机偏移量,从该位置开始遍历所有 P,尝试找到一个本地队列非空的 P 进行窃取。窃取时,会从目标 P 的本地队列尾部取走大约一半的 G。这种随机选择策略的好处是:(1) 避免了所有空闲 P 都去竞争同一个繁忙 P 造成的「惊群效应」;(2) 实现简单高效,不需要维护全局负载信息;(3) 在概率意义上能实现良好的负载均衡。选项 A 错误,因为维护「最长队列」需要全局视图,开销过大。选项 B 错误,固定顺序会导致排在前面的 P 总是被优先窃取,负载不均。选项 D 的合并操作在并发环境下既不安全也不高效,且开销过大。"