{ "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": [] } ] }