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": "code_reading",
|
||||
"schema_version": "1.0.0",
|
||||
"generated": "2026-09-09T16:21:56+08:00",
|
||||
"generated": "2026-09-13T00:00:00+08:00",
|
||||
"questions": [
|
||||
{
|
||||
"id": "cr-001",
|
||||
@@ -258,6 +258,143 @@
|
||||
"explanation": "本题考查Java线程池的核心参数和ForkJoinPool的工作原理。ThreadPoolExecutor通过核心线程数、最大线程数、队列和拒绝策略来控制任务执行。ForkJoinPool通过工作窃取算法实现更高效的并行计算,特别适合分治递归任务。",
|
||||
"source": null,
|
||||
"related": []
|
||||
},
|
||||
{
|
||||
"id": "cr-011",
|
||||
"type": "code_reading",
|
||||
"difficulty": 4,
|
||||
"tags": [
|
||||
"Go",
|
||||
"GMP",
|
||||
"GOMAXPROCS",
|
||||
"调度"
|
||||
],
|
||||
"question": "阅读以下 Go 代码。程序设置 GOMAXPROCS=1,启动了两个 goroutine 各自循环打印字母。分析程序的输出行为。",
|
||||
"code": "package main\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n)\n\nfunc main() {\n\truntime.GOMAXPROCS(1)\n\n\tgo func() {\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tfmt.Printf(\"A%d \", i)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tfmt.Printf(\"B%d \", i)\n\t\t}\n\t}()\n\n\tfmt.Println(\"main done\")\n}",
|
||||
"language": "go",
|
||||
"sub_questions": [
|
||||
{
|
||||
"index": 1,
|
||||
"type": "single_choice",
|
||||
"question": "关于这段代码的输出,以下哪项描述最准确?",
|
||||
"options": {
|
||||
"A": "必定先打印全部 A0~A4,再打印全部 B0~B4",
|
||||
"B": "A 和 B 的输出在任意时刻都是完全交错的(A0 B0 A1 B1 ...)",
|
||||
"C": "A 和 B 的输出会交替出现,但交替的位置不固定,且可能在任意 fmt.Printf 调用之间发生切换",
|
||||
"D": "两个 goroutine 的输出永远不会出现,因为 main 先结束"
|
||||
},
|
||||
"answer": "C",
|
||||
"explanation": "Go 1.14+ 采用基于信号的抢占式调度,即使 GOMAXPROCS=1 只有一个 P,调度器也会在函数调用点(如 fmt.Printf)之间抢占当前 goroutine,将执行权交给其他可运行的 goroutine。因此 A 和 B 会交替输出,但切换时机取决于调度器,不是严格的逐个交替。A 错误,因为抢占式调度不保证一个 goroutine 运行到结束;B 错误,交替不是严格一对一;D 错误,main 函数在启动 goroutine 后直接执行 fmt.Println 并退出,但 Go 运行时在 main 返回前会等待所有 goroutine 结束——不过这里 main 没有等待机制(如 sync.WaitGroup),实际上 main 会先打印 \"main done\" 然后程序退出,两个 goroutine 可能来不及执行。但题目聚焦调度行为,C 是对调度交替的最准确描述。"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"type": "short_answer",
|
||||
"question": "如果将 GOMAXPROCS 的值改为 2,程序的并发行为会发生什么本质变化?",
|
||||
"answer": "两个 goroutine 可以真正并行执行(在不同的 OS 线程上同时运行),而不再只是交替调度。",
|
||||
"explanation": "GOMAXPROCS=2 时运行时会创建两个 P,每个 P 绑定一个 M(OS 线程)。两个 goroutine 可以分别被分配到两个 P 上同时执行,实现真正的并行。GOMAXPROCS=1 时只有一个 P,任何时刻只有一个 goroutine 处于运行状态,goroutine 之间只能通过调度切换来交替执行(并发但不并行)。"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"type": "short_answer",
|
||||
"question": "GOMAXPROCS=1 时,goroutine 的调度切换可能发生在代码中的哪些位置?",
|
||||
"answer": "函数调用点(如 fmt.Printf)、channel 操作、系统调用、runtime.Gosched() 调用,以及 Go 1.14+ 中基于信号的异步抢占点。",
|
||||
"explanation": "Go 的调度器在以下位置可能切换 goroutine:1) 函数调用时插入的抢占检查点(cooperative preemption);2) Go 1.14+ 引入的基于 SIGURG 信号的异步抢占,可在任意指令处中断长时间运行的 goroutine;3) 主动让出(runtime.Gosched);4) 阻塞操作如 channel 读写、mutex 锁、系统调用等。"
|
||||
}
|
||||
],
|
||||
"explanation": "本题考察 GMP 模型中 GOMAXPROCS 对调度行为的影响。GOMAXPROCS 限制了 P 的数量,从而限制了并行度。GOMAXPROCS=1 时只有一个 P,所有 goroutine 在同一个 OS 线程上交替调度执行。Go 1.14+ 的信号抢占机制使得切换可以发生在几乎任意位置,但具体切换时机是不确定的。同时需要注意,本题中 main 函数没有等待 goroutine 的同步机制,实际运行中 main 可能先于其他 goroutine 退出。",
|
||||
"source": null,
|
||||
"related": []
|
||||
},
|
||||
{
|
||||
"id": "cr-012",
|
||||
"type": "code_reading",
|
||||
"difficulty": 4,
|
||||
"tags": [
|
||||
"Go",
|
||||
"GMP",
|
||||
"系统调用",
|
||||
"M/P hand-off"
|
||||
],
|
||||
"question": "阅读以下 Go 代码。程序启动了一个 goroutine 执行阻塞的文件读取操作,同时另一个 goroutine 在持续计算。分析 GMP 调度器如何处理阻塞的系统调用。",
|
||||
"code": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n)\n\nfunc main() {\n\truntime.GOMAXPROCS(1)\n\n\tgo func() {\n\t\tfmt.Println(\"[G1] starting blocking file read\")\n\t\tf, _ := os.Open(\"/dev/urandom\")\n\t\tbuf := make([]byte, 1024*1024)\n\t\tf.Read(buf) // potentially blocking syscall\n\t\tfmt.Println(\"[G1] read done\")\n\t\tf.Close()\n\t}()\n\n\tgo func() {\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tfmt.Printf(\"[G2] computing %d\\n\", i)\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t}\n\t}()\n\n\ttime.Sleep(2 * time.Second)\n\tfmt.Println(\"main exiting\")\n}",
|
||||
"language": "go",
|
||||
"sub_questions": [
|
||||
{
|
||||
"index": 1,
|
||||
"type": "single_choice",
|
||||
"question": "当 G1 执行 f.Read(buf) 进入阻塞系统调用时,GMP 调度器会发生什么?",
|
||||
"options": {
|
||||
"A": "G1 阻塞在系统调用中,P 继续绑定在原来的 M 上等待系统调用返回,G2 被阻塞无法执行",
|
||||
"B": "G1 和其 M 一起进入阻塞状态,P 被分离(hand-off)并关联到一个新的或空闲的 M 上,G2 继续在新 M 上执行",
|
||||
"C": "G1 的系统调用被放入后台线程,P 不做任何变化",
|
||||
"D": "调度器会阻止 G1 进入系统调用,转而让 G2 先执行"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "在 GMP 模型中,当一个 goroutine 进入阻塞系统调用时:1) 执行该系统调用的 M(OS 线程)会和 G 一起进入阻塞状态;2) 该 M 绑定的 P 会被分离(hand-off),P 的状态从 _Prunning 变为 _Psyscall;3) 调度器会将 P 分配给另一个空闲的 M(或创建新的 M),以便 P 上的其他可运行 goroutine(如 G2)能继续执行。这样确保阻塞的系统调用不会浪费 P 资源。当系统调用返回后,G1 会尝试重新获取一个 P,若获取不到则将自己放入全局运行队列。"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"type": "short_answer",
|
||||
"question": "系统调用返回后,G1 如何重新获得执行机会?请描述完整的恢复流程。",
|
||||
"answer": "G1 从系统调用返回后,首先尝试获取一个空闲的 P(在 hand-off 场景中通常会有一个 P 等待它)。如果成功获取 P,G1 在该 P 上继续执行;如果获取不到 P,G1 会被放入全局运行队列(Global Run Queue),等待某个 P 来调度执行。",
|
||||
"explanation": "具体流程:1) M1 上的系统调用返回,G1 恢复运行状态;2) G1 尝试获取一个空闲的 P(通过 tryacquirep 或查找 _Pidle 状态的 P);3) 如果成功,G1 绑定该 P 继续执行;4) 如果所有 P 都被占用,G1 的状态被设为 _Grunnable 并放入全局运行队列;5) 当某个 P 的本地队列为空时,它会从全局队列中取 G 来执行。在本题中,由于 GOMAXPROCS=1 且只有一个 G2 在运行,系统调用返回后 G1 通常能直接获取 P 继续执行。"
|
||||
}
|
||||
],
|
||||
"explanation": "本题考察 GMP 模型中系统调用阻塞时的 M/P hand-off 机制。这是 GMP 调度器的核心设计之一:当 M 因系统调用阻塞时,与其绑定的 P 会被分离并交给其他 M 使用,确保计算资源不被浪费。这个机制使得即使 GOMAXPROCS=1,一个 goroutine 的阻塞系统调用也不会阻塞其他 goroutine 的执行。对比线程模型中一个线程阻塞会浪费整个 CPU 核心的情况,GMP 的 hand-off 机制提供了更高效的阻塞处理。",
|
||||
"source": null,
|
||||
"related": []
|
||||
},
|
||||
{
|
||||
"id": "cr-013",
|
||||
"type": "code_reading",
|
||||
"difficulty": 4,
|
||||
"tags": [
|
||||
"Go",
|
||||
"GMP",
|
||||
"channel",
|
||||
"调度同步"
|
||||
],
|
||||
"question": "阅读以下 Go 代码。程序通过无缓冲 channel 在两个 goroutine 之间进行通信。分析 goroutine 的调度和阻塞行为。",
|
||||
"code": "package main\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n)\n\nfunc main() {\n\truntime.GOMAXPROCS(1)\n\n\tch := make(chan int) // unbuffered channel\n\n\tgo func() {\n\t\tfmt.Println(\"G1: before send\")\n\t\tch <- 42\n\t\tfmt.Println(\"G1: after send\")\n\t}()\n\n\tgo func() {\n\t\tfmt.Println(\"G2: before receive\")\n\t\tv := <-ch\n\t\tfmt.Printf(\"G2: received %d\\n\", v)\n\t}()\n\n\tfmt.Println(\"main: waiting\")\n\truntime.Gosched()\n\tfmt.Println(\"main: exiting\")\n}",
|
||||
"language": "go",
|
||||
"sub_questions": [
|
||||
{
|
||||
"index": 1,
|
||||
"type": "single_choice",
|
||||
"question": "假设 G1 先被调度执行,当 G1 执行到 ch <- 42 时,如果此时 G2 还没有执行到 <-ch,会发生什么?",
|
||||
"options": {
|
||||
"A": "G1 的发送操作立即成功,G1 继续执行下一行",
|
||||
"B": "G1 在 ch <- 42 处阻塞,被挂起并放入 ch 的发送者等待队列,调度器切换到其他可运行的 goroutine(如 G2)",
|
||||
"C": "G1 的发送操作会丢失数据,G1 继续执行",
|
||||
"D": "程序运行时 panic,因为没有接收者"
|
||||
},
|
||||
"answer": "B",
|
||||
"explanation": "无缓冲 channel 的发送操作是同步的:发送方必须等待接收方准备好。当 G1 执行 ch <- 42 但 G2 还没有执行到 <-ch 时:1) G1 的状态从 _Grunning 变为 _Gwaiting;2) G1 被挂起并放入 channel ch 的 sendq(发送者等待队列);3) G1 绑定的 P 被释放,调度器从 P 的本地队列中取下一个可运行的 G 执行(即 G2);4) 当 G2 执行到 <-ch 时,调度器直接将 G1 的值 42 传递给 G2,并唤醒 G1(将 G1 从 sendq 移到可运行队列)。"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"type": "single_choice",
|
||||
"question": "在 GOMAXPROCS=1 的情况下,以下哪种执行顺序是可能发生的?",
|
||||
"options": {
|
||||
"A": "main: waiting -> G1: before send -> G1 blocked at ch <- 42 -> G2: before receive -> G2: received 42 -> G1: after send -> main: exiting",
|
||||
"B": "main: waiting -> G1: before send -> G2: before receive -> G1: after send -> G2: received 42 -> main: exiting",
|
||||
"C": "G1: before send -> G2: before receive -> main: waiting -> G2: received 42 -> G1: after send -> main: exiting",
|
||||
"D": "以上所有顺序都是可能的"
|
||||
},
|
||||
"answer": "D",
|
||||
"explanation": "由于调度的不确定性,以下场景都是可能的:\n\n场景 A:main 先运行打印 \"main: waiting\",执行 runtime.Gosched() 让出,调度 G1,G1 打印后在 ch <- 42 处阻塞(G2 还没运行),调度切换到 G2,G2 接收后 G1 被唤醒,最后 main 恢复退出。\n\n场景 B:main 让出后 G1 被调度,打印后阻塞于发送,切换到 G2,但 G2 打印 \"before receive\" 后执行 <-ch 唤醒 G1,G1 和 G2 的后续打印顺序取决于调度器选择谁先执行。\n\n场景 C:G1 先运行(在 main 之前通过调度抢占),或 runtime.Gosched() 的效果导致调度顺序不同。main 的 runtime.Gosched() 会让出执行权,但不能保证 main 在其他 goroutine 之前打印 \"main: waiting\"(虽然概率很高)。\n\n实际上 B 和 C 的精确顺序取决于调度器实现细节,但就 channel 同步语义而言,它们都是合法的调度结果。"
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"type": "short_answer",
|
||||
"question": "如果将 ch := make(chan int) 改为 ch := make(chan int, 1)(缓冲大小为 1),G1 的阻塞行为会如何变化?",
|
||||
"answer": "G1 执行 ch <- 42 时不会阻塞,因为缓冲区有 1 个空位,数据直接放入缓冲区,G1 继续执行 \"G1: after send\"。G2 随后从缓冲区接收数据。",
|
||||
"explanation": "缓冲 channel 的容量为 1 时,只要缓冲区未满,发送操作就不需要等待接收方。G1 将 42 放入缓冲区后立即继续执行。当 G2 执行 <-ch 时,从缓冲区取出数据。如果 G2 先执行 <-ch(缓冲区为空),G2 会阻塞等待 G1 发送。这与无缓冲 channel 的根本区别在于:无缓冲 channel 要求发送方和接收方同时就绪(rendezvous),缓冲 channel 允许两者在时间上解耦。"
|
||||
}
|
||||
],
|
||||
"explanation": "本题考察 GMP 模型中无缓冲 channel 对 goroutine 调度的影响。无缓冲 channel 实现了 goroutine 之间的同步通信(rendezvous):发送方和接收方必须同时就绪才能完成数据传递。当一方先到达 channel 操作而另一方未就绪时,先到达的 goroutine 会被挂起(状态变为 _Gwaiting),放入 channel 的等待队列(sendq 或 recvq),P 被释放给其他可运行的 goroutine 使用。这种机制是 GMP 调度器处理同步阻塞的核心方式,与系统调用阻塞时的 M/P hand-off 类似,都确保 P 资源不被浪费。同时,由于 GOMAXPROCS=1,所有 goroutine 在同一个 P 上交替调度,进一步凸显了 channel 操作对调度顺序的影响。",
|
||||
"source": null,
|
||||
"related": []
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user