Files
examination/topics/interview-prep/go-java-concurrency/code_reading.json
T
wonder da9667c265
Deploy Examination / deploy (push) Successful in 4s
feat: add 23 GMP questions to go-java-concurrency
新增题目覆盖 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
2026-09-13 14:47:57 +08:00

400 lines
30 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"topic": "go-java-concurrency",
"type": "code_reading",
"schema_version": "1.0.0",
"generated": "2026-09-13T00:00:00+08:00",
"questions": [
{
"id": "cr-001",
"type": "code_reading",
"difficulty": 3,
"tags": [
"go",
"channel",
"select"
],
"question": "分析以下Go代码片段,理解select多路复用与channel交互行为:",
"code": "func main() {\n ch := make(chan int, 1)\n quit := make(chan struct{})\n go func() {\n time.Sleep(100 * time.Millisecond)\n ch <- 42\n close(quit)\n }()\n select {\n case v := <-ch:\n fmt.Println(\"received:\", v)\n case <-quit:\n fmt.Println(\"quit\")\n }\n // 第二个select\n select {\n case v := <-ch:\n fmt.Println(\"second:\", v)\n case <-quit:\n fmt.Println(\"quit again\")\n }\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "这段程序的输出最可能是什么?",
"options": {
"A": "received: 42,然后 quit again",
"B": "quit,然后 quit again",
"C": "received: 42,然后 second: 42",
"D": "编译错误"
},
"answer": "A",
"explanation": "第一个select等待100ms后goroutine向ch发送42并close(quit)。由于ch有缓冲1,send先于close执行,第一个select匹配case v:=<-ch,输出'received: 42'。之后ch已被消费为空,quit已关闭。第二个select时ch为空无法接收,quit已关闭可立即接收,输出'quit again'。"
},
{
"index": 2,
"type": "short_answer",
"question": "如果将ch的缓冲大小从1改为0(无缓冲channel),程序的行为会有什么变化?",
"answer": "第一个select可能匹配quit分支而非ch分支",
"keywords": [
"无缓冲",
"同步阻塞",
"send和receive必须同时就绪",
"竞争"
],
"scoring_rubric": "正确指出无缓冲channel需要发送方和接收方同时就绪(2分),说明goroutine中send在close之前但第一个select可能先匹配quit(1分),说明行为不确定/依赖调度(1分)",
"explanation": "无缓冲channel的send操作必须有对应的receive就绪才能完成。goroutine执行ch <- 42时会阻塞,直到主goroutine的select准备接收。但select也会检查quit channel(此时quit未close),所以send和select存在竞争。如果select先选中quit分支则退出,否则选中ch分支完成send。行为不确定。"
}
],
"explanation": "本题考查Go channel的select多路复用机制。select会同时监听所有case,当多个case就绪时随机选择一个。有缓冲channel的send不阻塞(缓冲区未满),而无缓冲channel的send必须有对应的receive。close已关闭的channel会立即返回零值。",
"source": null,
"related": []
},
{
"id": "cr-002",
"type": "code_reading",
"difficulty": 4,
"tags": [
"go",
"sync",
"rwmutex"
],
"question": "分析以下Go代码片段,找出sync.RWMutex使用中的潜在问题:",
"code": "type Cache struct {\n mu sync.RWMutex\n data map[string]string\n}\n\nfunc (c *Cache) Get(key string) string {\n c.mu.RLock()\n defer c.mu.RUnlock()\n if v, ok := c.data[key]; ok {\n return v\n }\n c.mu.RUnlock()\n c.mu.Lock()\n defer c.mu.Unlock()\n // double check\n if v, ok := c.data[key]; ok {\n return v\n }\n c.data[key] = \"default\"\n return \"default\"\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "这段代码在key不存在时会出现什么问题?",
"options": {
"A": "死锁,因为重复调用了RUnlock",
"B": "panic: sync: unlock of unlocked RWMutex",
"C": "正常运行,无任何问题",
"D": "数据竞争,但不会崩溃"
},
"answer": "B",
"explanation": "当key不存在时,第一个RLock/RUnlock的defer已经unlock了读锁,接着又显式调用c.mu.RUnlock()试图再次释放读锁,导致panic: sync: unlock of unlocked RWMutex。即使没有defer,RUnlock后再次RUnlock也会panic。"
},
{
"index": 2,
"type": "short_answer",
"question": "请写出修正后的Get方法,正确实现读锁升级为写锁的逻辑。",
"answer": "func (c *Cache) Get(key string) string {\n c.mu.RLock()\n if v, ok := c.data[key]; ok {\n c.mu.RUnlock()\n return v\n }\n c.mu.RUnlock()\n c.mu.Lock()\n defer c.mu.Unlock()\n if v, ok := c.data[key]; ok {\n return v\n }\n c.data[key] = \"default\"\n return \"default\"\n}",
"keywords": [
"先释放读锁",
"再获取写锁",
"double check",
"defer释放"
],
"scoring_rubric": "正确释放读锁后再获取写锁(2分),保留double check避免重复写(1分),写操作在写锁保护下(1分),使用defer释放锁(1分)",
"explanation": "Go的RWMutex不支持锁升级(直接从读锁升级为写锁)。正确做法是先释放读锁,再获取写锁,然后进行double check确认数据是否已被其他goroutine写入。"
}
],
"explanation": "本题考查RWMutex的正确使用模式。Go的sync.RWMutex不支持锁升级,读锁和写锁互斥,必须先释放读锁再获取写锁。常见的错误是重复释放锁导致panic。",
"source": null,
"related": []
},
{
"id": "cr-003",
"type": "code_reading",
"difficulty": 4,
"tags": [
"go",
"goroutine",
"pprof"
],
"question": "分析以下Go代码片段,识别goroutine泄漏的场景:",
"code": "func queryAll(urls []string) []Result {\n results := make(chan Result, len(urls))\n for _, url := range urls {\n go func(u string) {\n resp, err := http.Get(u)\n if err != nil {\n results <- Result{URL: u, Err: err}\n return\n }\n defer resp.Body.Close()\n body, _ := io.ReadAll(resp.Body)\n results <- Result{URL: u, Body: body}\n }(url)\n }\n var collected []Result\n for i := 0; i < len(urls); i++ {\n collected = append(collected, <-results)\n }\n return collected\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "如果调用queryAll时传入的urls切片包含100个URL,但http.Get对某些URL长时间超时未响应,会发生什么?",
"options": {
"A": "queryAll会立即返回空结果",
"B": "queryAll会阻塞直到所有goroutine完成(包括超时的)",
"C": "超时的goroutine会被自动回收",
"D": "程序会panic"
},
"answer": "B",
"explanation": "主goroutine的for循环会依次从results channel接收len(urls)个结果。如果某些goroutine因网络超时长时间阻塞在http.Get上,主goroutine会在对应的<-results处阻塞等待,直到所有goroutine都完成。没有设置context超时控制。"
},
{
"index": 2,
"type": "short_answer",
"question": "如何改进这段代码以避免goroutine泄漏?至少给出两种方案。",
"answer": "方案一:使用context.WithTimeout控制整体超时。方案二:使用select+time.After或context控制单个请求超时。方案三:使用errgroup管理goroutine。",
"keywords": [
"context.WithTimeout",
"http.NewRequestWithContext",
"errgroup",
"select",
"超时控制"
],
"scoring_rubric": "提出context超时方案(2分),说明具体实现方式(2分),解释为什么能防止泄漏(1分)",
"explanation": "方案1:使用context.WithTimeout创建带超时的context传给http.NewRequestWithContext,超时后HTTP请求取消。方案2:使用errgroup.Group的WithContext方法自动管理goroutine生命周期。方案3:增加done channel或select+timer实现超时退出。"
},
{
"index": 3,
"type": "single_choice",
"question": "使用pprof定位此类goroutine泄漏时,最应该查看哪个profile?",
"options": {
"A": "cpu profile",
"B": "heap profile",
"C": "goroutine profile",
"D": "block profile"
},
"answer": "C",
"explanation": "goroutine profile可以显示当前所有goroutine的调用栈,能直接看到泄漏的goroutine数量和它们阻塞在哪个函数调用上,是排查goroutine泄漏的首选工具。block profile显示锁竞争和channel阻塞,但不如goroutine profile直观。"
}
],
"explanation": "本题考查goroutine泄漏的典型场景——缺乏超时控制导致goroutine无法退出。正确做法是通过context控制请求超时,并使用pprof的goroutine profile定位泄漏。",
"source": null,
"related": []
},
{
"id": "cr-004",
"type": "code_reading",
"difficulty": 4,
"tags": [
"java",
"volatile",
"jmm"
],
"question": "分析以下Java代码片段,理解volatile语义与happens-before规则:",
"code": "public class VisibilityExample {\n private volatile boolean running = true;\n private int counter = 0;\n\n public void startLoop() {\n new Thread(() -> {\n while (running) {\n counter++;\n }\n System.out.println(\"Stopped. counter=\" + counter);\n }).start();\n }\n\n public void stop() {\n running = false;\n }\n}",
"language": "java",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "关于这段代码,以下哪个说法是正确的?",
"options": {
"A": "running是volatile的,所以counter++也是线程安全的",
"B": "running的修改能被子线程看到,但counter可能存在可见性问题",
"C": "由于while循环的存在,子线程永远无法看到running=false",
"D": "这段代码完全没有并发问题"
},
"answer": "B",
"explanation": "volatile保证running的修改对子线程可见(happens-before语义),所以子线程最终会退出循环。但counter不是volatile的,counter++(读-改-写)不是原子操作,子线程读取counter的值可能存在可见性问题——子线程可能一直看到旧值。不过由于只有一个线程修改counter,实际不会有数据竞争。选项B正确指出counter可能存在可见性问题(虽然单线程写不会出错)。"
},
{
"index": 2,
"type": "short_answer",
"question": "如果在main线程中频繁调用stop()方法,而同时子线程在执行counter++,counter的最终值是否准确?请从JMM的角度解释原因。",
"answer": "counter的值不保证准确。counter++不是原子操作,包含read、increment、write三步。虽然本例中只有子线程一个线程修改counter,但如果counter被多个线程修改,就会出现数据竞争。从JMM角度看,非volatile变量的修改不保证对其他线程可见。本例中只有一个线程写counter,所以值是准确的,但如果设计意图是多个线程共享counter,则需要AtomicInteger或synchronized。",
"keywords": [
"原子性",
"read-modify-write",
"volatile不保证原子性",
"AtomicInteger",
"数据竞争"
],
"scoring_rubric": "指出counter++非原子操作(2分),说明volatile不保证原子性(1分),提出AtomicInteger/synchronized解决方案(1分),区分单线程写和多线程写场景(1分)",
"explanation": "volatile只保证可见性和有序性,不保证原子性。counter++是复合操作(读-改-写),在多线程环境下会出现竞态条件。应使用AtomicInteger的getAndIncrement()或synchronized保护counter。"
}
],
"explanation": "本题考查Java内存模型中volatile的核心语义。volatile保证变量的可见性(一个线程的修改对其他线程可见)和有序性(禁止指令重排序),但不保证原子性。counter++这样的复合操作仍需要额外同步机制。",
"source": null,
"related": []
},
{
"id": "cr-005",
"type": "code_reading",
"difficulty": 5,
"tags": [
"java",
"thread",
"forkjoinpool"
],
"question": "分析以下Java代码片段,理解ThreadPoolExecutor与ForkJoinPool的区别:",
"code": "// 方案A: ThreadPoolExecutor\nExecutorService poolA = new ThreadPoolExecutor(\n 4, 8, 60L, TimeUnit.SECONDS,\n new LinkedBlockingQueue<>(1000)\n);\n\n// 方案B: ForkJoinPool\nForkJoinPool poolB = new ForkJoinPool(\n Runtime.getRuntime().availableProcessors()\n);\n\n// 提交任务\npoolA.submit(() -> {\n // CPU密集型计算\n return heavyComputation();\n});\n\npoolB.submit(() -> {\n // 可分治的CPU密集型计算\n return recursiveComputation();\n});",
"language": "java",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "当poolA的线程都在执行任务且队列已满时,新提交的任务会怎样?",
"options": {
"A": "创建新的线程来执行",
"B": "任务在调用者的线程中执行(CallerRunsPolicy)",
"C": "抛出RejectedExecutionException(使用默认拒绝策略时)",
"D": "任务被丢弃但不抛异常"
},
"answer": "C",
"explanation": "ThreadPoolExecutor的默认拒绝策略是AbortPolicy,当线程数达到maximumPoolSize且工作队列已满时,会抛出RejectedExecutionException。注意:本例中核心线程数4 < 最大线程数8,队列是无界的(capacity=1000),但实际上LinkedBlockingQueue(1000)有界。当8个线程都在忙且队列1000个位置都满了,才会触发拒绝策略。"
},
{
"index": 2,
"type": "single_choice",
"question": "ForkJoinPool相比ThreadPoolExecutor的核心优势是什么?",
"options": {
"A": "支持更多的线程数",
"B": "工作窃取算法允许空闲线程从其他线程的队列中取任务,提高CPU利用率",
"C": "不需要传入Runnable或Callable任务",
"D": "自动管理线程的生命周期"
},
"answer": "B",
"explanation": "ForkJoinPool的核心优势是工作窃取(work-stealing)算法。每个工作线程有自己的双端队列(Deque),当一个线程的队列为空时,它可以'窃取'其他线程队列尾部的任务。这比ThreadPoolExecutor的单一共享队列减少了锁竞争,特别适合递归分治任务(如ForkJoinTask的fork/join模式)。"
},
{
"index": 3,
"type": "short_answer",
"question": "在什么场景下应该选择ForkJoinPool而不是ThreadPoolExecutor?请举例说明。",
"answer": "适合ForkJoinPool的场景:1.任务可以分解为子任务的分治问题(如排序、搜索);2.大量小任务且执行时间差异大(工作窃取能平衡负载);3.递归任务。适合ThreadPoolExecutor的场景:1.独立的异步任务(如HTTP请求);2.任务之间没有依赖关系;3.需要精确控制队列和拒绝策略。",
"keywords": [
"分治",
"递归",
"工作窃取",
"负载均衡",
"独立任务",
"队列策略"
],
"scoring_rubric": "正确描述ForkJoinPool适用场景(2分),正确描述ThreadPoolExecutor适用场景(1分),给出具体例子(1分),提到工作窃取的负载均衡优势(1分)",
"explanation": "ForkJoinPool设计用于可递归分解的任务,其工作窃取算法在任务执行时间不均匀时表现优异。ThreadPoolExecutor更通用,适合独立的异步任务。Java 8+的parallelStream底层就使用ForkJoinPool.commonPool()。"
}
],
"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": []
}
]
}