feat: add 23 GMP questions to go-java-concurrency
Deploy Examination / deploy (push) Successful in 4s
Deploy Examination / deploy (push) Successful in 4s
新增题目覆盖 Go GMP 调度模型核心知识点: - 单选 ×10: G状态机、M角色、GOMAXPROCS、netpoller、信号抢占、 runtime.schedule、work-stealing、连续栈、sysmon、channel调度 - 判断 ×5: G状态转换、SIGURG异步抢占、本地队列容量、hand-off、栈保护 - 填空 ×5: GOMAXPROCS默认值、调度顺序、stackguard0、SIGURG、GOMAXPROCS(0) - 代码阅读 ×3: 调度交错、M/P hand-off、无缓冲channel同步 Total: 45 → 68 questions
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"topic": "go-java-concurrency",
|
||||
"type": "single_choice",
|
||||
"schema_version": "1.0.0",
|
||||
"generated": "2026-09-09T16:21:56+08:00",
|
||||
"generated": "2026-09-13T00:00:00+08:00",
|
||||
"questions": [
|
||||
{
|
||||
"id": "sc-001",
|
||||
@@ -327,6 +327,186 @@
|
||||
"explanation": "ForkJoinPool 采用工作窃取算法:每个工作线程维护自己的双端队列(deque),从头部取任务执行(FIFO);当自己的队列为空时,从其他忙碌线程的队列尾部窃取任务(LIFO),减少竞争。这种设计比传统的线程池(单个共享队列)更高效,因为减少了锁争用。ForkJoinPool 是 parallelStream() 的底层线程池,commonPool 是共享的 ForkJoinPool 实例。RecursiveTask(有返回值)和 RecursiveAction(无返回值)是 fork/join 编程模型的任务基类。",
|
||||
"source": null,
|
||||
"related": []
|
||||
},
|
||||
{
|
||||
"id": "sc-016",
|
||||
"type": "single_choice",
|
||||
"difficulty": 3,
|
||||
"tags": [
|
||||
"Go",
|
||||
"GMP"
|
||||
],
|
||||
"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 的关键机制。"
|
||||
},
|
||||
{
|
||||
"id": "sc-017",
|
||||
"type": "single_choice",
|
||||
"difficulty": 3,
|
||||
"tags": [
|
||||
"Go",
|
||||
"GMP"
|
||||
],
|
||||
"question": "在 GMP 调度模型中,关于 M(Machine)与 P(Processor)的绑定关系,以下描述正确的是?",
|
||||
"options": {
|
||||
"A": "M 与 P 是一一绑定的,创建 M 时必须同时创建一个 P",
|
||||
"B": "M 是操作系统线程,可以不与任何 P 绑定,此时 M 处于休眠状态",
|
||||
"C": "P 的数量固定为 CPU 核心数,无法通过代码修改",
|
||||
"D": "M 的数量受到 P 的数量严格限制,不可能超过 P 的数量"
|
||||
},
|
||||
"answer": "B",
|
||||
"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 的利用率。"
|
||||
},
|
||||
{
|
||||
"id": "sc-018",
|
||||
"type": "single_choice",
|
||||
"difficulty": 2,
|
||||
"tags": [
|
||||
"Go",
|
||||
"GMP"
|
||||
],
|
||||
"question": "调用 runtime.GOMAXPROCS(4) 后,以下关于其行为的描述哪一个是正确的?",
|
||||
"options": {
|
||||
"A": "它限制了整个程序中同时存在的 goroutine 总数最多为 4",
|
||||
"B": "它将 P 的数量设置为 4,表示最多有 4 个 goroutine 可以并行执行",
|
||||
"C": "它将 M(系统线程)的数量限制为 4",
|
||||
"D": "它创建了 4 个 P 并且这个值之后不可再更改"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "runtime.GOMAXPROCS(n) 用于设置 P(逻辑处理器)的数量,其默认值等于 GOMAXPROCS 环境变量或机器的 CPU 核心数。P 是真正执行 goroutine 代码的资源,因此 P 的数量决定了真正并行执行的 goroutine 数上限。选项 A 错误,GOMAXPROCS 不限制 goroutine 的总数——程序可以创建数百万个 goroutine,只是同一时刻最多只有 GOMAXPROCS 个在真正并行运行。选项 C 错误,M 的数量由运行时根据需要自动管理,不受 GOMAXPROCS 限制。选项 D 错误,GOMAXPROCS 可以在运行时多次调用动态修改,每次调用都会调整 P 的数量,返回值是调用前的旧值。"
|
||||
},
|
||||
{
|
||||
"id": "sc-019",
|
||||
"type": "single_choice",
|
||||
"difficulty": 4,
|
||||
"tags": [
|
||||
"Go",
|
||||
"GMP"
|
||||
],
|
||||
"question": "Go 运行时中的 netpoller 在调度过程中扮演什么角色?",
|
||||
"options": {
|
||||
"A": "netpoller 负责将阻塞在网络 I/O 的系统调用转换为协程切换,减少 M 的消耗",
|
||||
"B": "netpoller 通过 epoll/kqueue 等机制实现异步网络 I/O,将就绪的 G 加入运行队列,避免它们占用 M 进入系统调用状态",
|
||||
"C": "netpoller 仅在 sysmon 中被调用,用于定期检查网络事件",
|
||||
"D": "netpoller 是用户空间的网络库,与运行时调度器完全独立"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "netpoller 是 Go 运行时中负责异步网络 I/O 的核心组件。它利用操作系统提供的 I/O 多路复用机制(Linux 上是 epoll,macOS 上是 kqueue,Windows 上是 IOCP)来实现非阻塞网络操作。当 goroutine 执行网络 I/O 时,运行时不会让 M 和 P 陷入真正的阻塞系统调用,而是将该 G 注册到 netpoller 后置为 _Gwaiting 状态,释放 P 让其继续服务其他 G。当网络事件就绪时,netpoller 会将对应的 G 标记为 _Grunnable 并放入运行队列。选项 A 描述不够准确,netpoller 并非\"转换\"系统调用,而是从根本上避免了阻塞式网络系统调用。选项 C 错误,虽然 sysmon 会唤醒 netpoller,但 netpoller 并非只在 sysmon 中调用——调度器的 findrunnable 等路径也会主动检查 netpoller。选项 D 错误,netpoller 是运行时的内部组件,深度集成于调度器中。"
|
||||
},
|
||||
{
|
||||
"id": "sc-020",
|
||||
"type": "single_choice",
|
||||
"difficulty": 4,
|
||||
"tags": [
|
||||
"Go",
|
||||
"GMP"
|
||||
],
|
||||
"question": "Go 1.14 引入了基于信号的异步抢占机制。关于此机制,以下哪一项描述是正确的?",
|
||||
"options": {
|
||||
"A": "异步抢占使用 SIGINT 信号通知 goroutine 让出 CPU",
|
||||
"B": "异步抢占使得长时间运行且无函数调用的纯计算 goroutine 也能被抢占,解决了之前的协作式抢占的不足",
|
||||
"C": "异步抢占完全取代了协作式抢占,Go 1.14 之后不再使用函数调用前的抢占检查点",
|
||||
"D": "异步抢占需要程序员手动在代码中插入抢占点才能生效"
|
||||
},
|
||||
"answer": "B",
|
||||
"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 的合并操作在并发环境下既不安全也不高效,且开销过大。"
|
||||
},
|
||||
{
|
||||
"id": "sc-023",
|
||||
"type": "single_choice",
|
||||
"difficulty": 4,
|
||||
"tags": [
|
||||
"Go",
|
||||
"GMP"
|
||||
],
|
||||
"question": "Go 的栈增长机制经历了从分段栈(segmented stack)到连续栈(contiguous stack)的演进。关于连续栈,以下哪一项描述是正确的?",
|
||||
"options": {
|
||||
"A": "连续栈在栈空间不足时直接分配一块更大的栈,然后将整个栈内容拷贝过去",
|
||||
"B": "连续栈通过链表将多个栈片段串联起来,与分段栈的区别仅在于管理方式",
|
||||
"C": "连续栈在栈扩容时分配两倍大小的新栈,通过调整栈帧中的指针来完成迁移,避免了 hot-split 问题",
|
||||
"D": "连续栈只在编译期分配固定大小,运行时无法扩展"
|
||||
},
|
||||
"answer": "C",
|
||||
"explanation": "Go 1.4 起采用连续栈(contiguous stack,也称 copystack)取代了分段栈。其核心机制是:当 goroutine 的栈空间不足时,分配一块 2 倍大小的新栈,将旧栈内容完整拷贝到新栈中,然后逐一调整栈帧中所有指向旧栈的指针使其指向新栈的对应位置。这要求运行时精确知道栈中所有指针的位置(通过编译器生成的栈映射信息)。选项 A 的描述不完整,关键不只是「拷贝」,还有指针调整,如果只拷贝不调整指针会导致悬空引用。选项 B 错误,连续栈的「连续」正是强调栈内存是连续的一块,不需要链表串联——链表串联恰恰是分段栈的特征,分段栈有 hot-split 问题:在栈边界反复调用返回时会频繁触发栈的分裂与合并。选项 D 完全错误,连续栈正是为了解决运行时动态扩展而设计的。"
|
||||
},
|
||||
{
|
||||
"id": "sc-024",
|
||||
"type": "single_choice",
|
||||
"difficulty": 5,
|
||||
"tags": [
|
||||
"Go",
|
||||
"GMP"
|
||||
],
|
||||
"question": "Go 运行时中的 sysmon 线程负责多项后台职责。以下哪一项**不是** sysmon 的职责?",
|
||||
"options": {
|
||||
"A": "检测并抢占长时间运行的 goroutine(retake)",
|
||||
"B": "触发垃圾回收(GC)的定时检测",
|
||||
"C": "轮询 netpoll 以获取就绪的 G 并放入运行队列",
|
||||
"D": "管理 goroutine 的创建和销毁,维护 G 的对象池"
|
||||
},
|
||||
"answer": "D",
|
||||
"explanation": "sysmon 是 Go 运行时中的一个特殊后台监控线程,它不与任何 P 绑定,独立于 GMP 调度体系运行。其主要职责包括:(1) retake(抢占)——检测长时间运行(超过 10ms)的 G 或长时间处于系统调用中的 P,通过异步抢占让出 CPU 或回收 P。(2) GC 触发——定期检查是否需要触发新一轮垃圾回收,特别是当 GC 已经超过一定时间未执行时。(3) netpoll——周期性地唤醒 netpoller 检查网络事件,将就绪的 G 放入运行队列,确保网络 I/O 不会因缺乏主动调用而延迟。(4) 释放长时间未使用的物理内存(如通过 MADV_FREE 通知操作系统)。选项 D 描述的「管理 goroutine 的创建和销毁」不是 sysmon 的职责——G 的创建由用户代码通过 go 关键字触发,由运行时的 newproc 函数处理;G 的销毁由 G 自行退出时的 goexit 函数处理,与 sysmon 无关。"
|
||||
},
|
||||
{
|
||||
"id": "sc-025",
|
||||
"type": "single_choice",
|
||||
"difficulty": 4,
|
||||
"tags": [
|
||||
"Go",
|
||||
"GMP"
|
||||
],
|
||||
"question": "以下关于 GMP 调度中 channel 操作导致的 G 状态转换,哪一项描述是正确的?",
|
||||
"options": {
|
||||
"A": "当 goroutine 执行 ch <- val 发送数据到一个已满的 buffered channel 时,该 G 的状态从 _Grunning 转为 _Gsyscall",
|
||||
"B": "当 goroutine 执行 val := <-ch 从一个空的 channel 接收数据时,该 G 的状态从 _Grunning 转为 _Gwaiting,并被放入 channel 的接收队列",
|
||||
"C": "当 channel 的对端操作完成时(如发送方写入数据),等待中的 G 状态直接从 _Gwaiting 转为 _Grunning",
|
||||
"D": "channel 操作涉及的状态转换与 select 语句中的多路复用完全无关,select 不会改变 G 的状态"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "当 goroutine 执行 channel 的接收操作(val := <-ch)而 channel 为空时,当前 G 会被挂起:状态从 _Grunning 转为 _Gwaiting,并被放入 channel 内部维护的 recvq(接收等待队列)中,同时释放 P 让其调度其他 G。当后续某个 goroutine 向该 channel 发送数据时,运行时会从 recvq 中取出一个等待的 G,将数据直接拷贝给它,并将该 G 的状态从 _Gwaiting 转为 _Grunnable(可运行),放入运行队列等待被调度。选项 A 错误,channel 操作导致的是 _Gwaiting 而非 _Gsyscall——_Gsyscall 专用于系统调用场景,channel 操作属于用户态的同步原语。选项 C 不准确,等待中的 G 状态先转为 _Grunnable(放入运行队列),然后被调度器选中时才转为 _Grunning,而非直接从 _Gwaiting 跳到 _Grunning。选项 D 完全错误,select 语句在多个 channel 上的等待同样会导致 G 进入 _Gwaiting 状态,且 select 会将 G 同时放入多个 channel 的等待队列。"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user