Files
examination/topics/interview-prep/go-java-concurrency/single_choice.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

512 lines
33 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": "single_choice",
"schema_version": "1.0.0",
"generated": "2026-09-13T00:00:00+08:00",
"questions": [
{
"id": "sc-001",
"type": "single_choice",
"difficulty": 2,
"tags": [
"Go",
"GMP",
"调度模型"
],
"question": "在 Go 的 GMP 调度模型中,P(Processor)代表什么?",
"options": {
"A": "操作系统线程(OS Thread)",
"B": "逻辑处理器,持有本地运行队列和资源上下文",
"C": "用户级轻量级线程(goroutine)",
"D": "内存管理单元(MMU)"
},
"answer": "B",
"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))在缓冲区未满时发送不会阻塞,这是两者的关键区别。",
"source": null,
"related": []
},
{
"id": "sc-005",
"type": "single_choice",
"difficulty": 4,
"tags": [
"Go",
"channel",
"hchan",
"底层结构"
],
"question": "Go channel 的底层数据结构 hchan 中,sendx 和 recvx 字段的作用是什么?",
"options": {
"A": "记录 channel 的发送和接收总次数,用于性能统计",
"B": "环形缓冲区中下一个发送/接收位置的索引,用于实现 FIFO 语义",
"C": "记录当前正在阻塞的发送者/接收者数量",
"D": "指向 channel 类型元信息的指针偏移量"
},
"answer": "B",
"explanation": "hchan 是 Go channel 的底层结构体。其中 buf 是一个环形缓冲区(用于有缓冲 channel),sendx 和 recvx 分别记录下一次发送和接收在 buf 中的索引位置,实现 FIFO(先进先出)语义。当 sendx 或 recvx 到达缓冲区末尾时会回绕到 0。与之相关的字段还有 sendq(等待发送的 goroutine 队列,即 sudog 链表)和 recvq(等待接收的 goroutine 队列)。",
"source": null,
"related": []
},
{
"id": "sc-006",
"type": "single_choice",
"difficulty": 2,
"tags": [
"Go",
"channel",
"close"
],
"question": "关于 Go channel 的 close 语义,以下哪项描述是正确的?",
"options": {
"A": "对一个已关闭的 channel 再次调用 close 会 panic",
"B": "关闭一个 nil channel 会静默忽略,不会 panic",
"C": "关闭 channel 后,仍然可以向其发送数据",
"D": "关闭 channel 后,接收操作会立即阻塞"
},
"answer": "A",
"explanation": "Go 对 channel 的关闭有严格的规则:1)对已关闭的 channel 再次 close 会触发 panic(send on closed channel 的同类保护);2)对 nil channel 调用 close 同样会 panic;3)关闭 channel 后不能再发送数据(会 panic),但可以继续接收——接收操作会返回缓冲区中的剩余数据,缓冲区读完后接收操作返回零值且 ok 为 false;4)只有发送方或唯一所有者应该关闭 channel,不应由接收方关闭。",
"source": null,
"related": []
},
{
"id": "sc-007",
"type": "single_choice",
"difficulty": 3,
"tags": [
"Go",
"sync",
"Mutex",
"RWMutex"
],
"question": "Go 的 sync.RWMutex 相比 sync.Mutex 的主要优势是什么?",
"options": {
"A": "在写锁模式下性能更好",
"B": "允许多个 goroutine 同时持有读锁,提高读多写少场景的并发性能",
"C": "支持可重入锁,同一个 goroutine 可多次加锁",
"D": "使用无锁算法实现,完全不涉及操作系统原语"
},
"answer": "B",
"explanation": "RWMutex 提供了读写分离的锁机制:多个 goroutine 可以同时持有 RLock(读锁),只有写锁是互斥的。这在读多写少的场景下显著优于 Mutex(所有操作互斥)。选项 A 错误,RWMutex 的写锁性能通常略低于纯 Mutex,因为需要维护读写计数。选项 C 错误,Go 的 Mutex 和 RWMutex 都不是可重入的,重复加锁会导致死锁。选项 D 错误,底层仍使用原子操作和信号量。",
"source": null,
"related": []
},
{
"id": "sc-008",
"type": "single_choice",
"difficulty": 3,
"tags": [
"Go",
"sync",
"Once",
"Pool"
],
"question": "关于 Go 的 sync.Once 和 sync.Pool,以下哪项描述是正确的?",
"options": {
"A": "sync.Once 的 Do 方法在并发调用时,只有第一个调用会执行传入的函数,其余阻塞等待",
"B": "sync.Pool 中的对象永远不会被回收,适合存放全局唯一实例",
"C": "sync.Once 的 Do 方法中如果传入的函数 panic,后续调用会重新执行该函数",
"D": "sync.Pool 是线程安全的,但不支持跨 goroutine 共享对象"
},
"answer": "A",
"explanation": "sync.Once 的 Do(f) 保证 f 只被执行一次,即使多个 goroutine 并发调用。第一个调用者执行 f,其他调用者阻塞等待 f 完成后直接返回。选项 B 错误,Pool 中的对象在每次 GC 时可能被清除(Go 1.13 后有 victim cache 机制,延迟一个 GC 周期),不适合存放必须持久化的对象。选项 C 错误,如果 f panic,Once 仍然认为已执行过,后续调用不会再执行 f。选项 D 错误,Pool 设计上就是跨 goroutine 共享的,用于对象复用以减少 GC 压力。",
"source": null,
"related": []
},
{
"id": "sc-009",
"type": "single_choice",
"difficulty": 4,
"tags": [
"Go",
"sync",
"Map",
"并发"
],
"question": "Go 的 sync.Map 适合以下哪种使用场景?",
"options": {
"A": "频繁写入、少量读取的场景",
"B": "key 集合稳定,大量并发读取、极少写入的场景",
"C": "需要有序遍历所有 key-value 的场景",
"D": "需要支持批量删除操作的场景"
},
"answer": "B",
"explanation": "sync.Map 针对两种场景优化:1)key 集合稳定后只读(read-only);2)多个 goroutine 读写不同的 key(disjoint key sets)。它内部使用 read(只读,无锁访问)和 dirty(写锁保护)两个 map 实现读写分离,在大量并发读的场景下性能远优于 Mutex+map。选项 A 错误,频繁写入会导致 read/dirty 频繁同步,性能反而不如加锁。选项 C 错误,sync.Map 不保证遍历顺序。选项 D 错误,sync.Map 不提供批量删除接口。",
"source": null,
"related": []
},
{
"id": "sc-010",
"type": "single_choice",
"difficulty": 3,
"tags": [
"Go",
"内存模型",
"happens-before",
"race"
],
"question": "Go 的 -race 检测器主要基于什么技术实现?",
"options": {
"A": "静态代码分析,在编译期检测所有潜在的数据竞争",
"B": "AddressSanitizer(ASan),在运行时检测内存越界访问",
"C": "ThreadSanitizer(TSan),在运行时动态追踪内存访问的 happens-before 关系",
"D": "在每个内存访问处插入互斥锁,通过死锁检测间接发现竞争"
},
"answer": "C",
"explanation": "Go 的 race detector 基于 Google 的 ThreadSanitizer(TSan)技术,在编译期插桩(instrumentation)并在运行时动态追踪所有内存访问和同步操作,构建 happens-before 关系图。如果两个并发访问(至少一个是写)之间没有 happens-before 关系,就报告数据竞争。选项 A 错误,它不是纯静态分析,需要运行时执行才能检测。选项 B 错误,ASan 检测的是内存安全(越界、use-after-free),不是数据竞争。选项 D 完全不正确。",
"source": null,
"related": []
},
{
"id": "sc-011",
"type": "single_choice",
"difficulty": 4,
"tags": [
"Go",
"goroutine泄漏",
"pprof",
"context"
],
"question": "以下哪种情况最容易导致 goroutine 泄漏?",
"options": {
"A": "goroutine 中执行了一次短暂的 CPU 计算后正常返回",
"B": "goroutine 向一个没有接收者的无缓冲 channel 发送数据",
"C": "goroutine 调用了 time.Sleep 后正常退出",
"D": "goroutine 中使用了 defer 语句"
},
"answer": "B",
"explanation": "goroutine 泄漏是指 goroutine 无法正常退出,持续占用内存和调度资源。向无缓冲 channel 发送数据时,如果没有接收者,发送操作会永久阻塞,goroutine 就泄漏了。常见的泄漏模式还包括:1)从无人发送的 channel 接收;2)未设置超时或取消的阻塞操作(如未用 context 控制的 HTTP 请求);3)无限循环且无退出条件。排查工具:pprof 的 goroutine profile 可以看到所有存活 goroutine 的堆栈,定位泄漏源。",
"source": null,
"related": []
},
{
"id": "sc-012",
"type": "single_choice",
"difficulty": 3,
"tags": [
"Java",
"线程",
"ThreadPoolExecutor"
],
"question": "Java ThreadPoolExecutor 的核心参数 corePoolSize、maximumPoolSize、workQueue 之间的执行关系是?",
"options": {
"A": "任务提交后直接创建线程,不受 corePoolSize 限制",
"B": "先创建到 corePoolSize 个线程 → 放入 workQueue → 满了才创建到 maximumPoolSize 个线程",
"C": "先创建到 maximumPoolSize 个线程 → 放入 workQueue → 满了才缩减到 corePoolSize",
"D": "workQueue 仅在 corePoolSize 和 maximumPoolSize 都满时才使用"
},
"answer": "B",
"explanation": "ThreadPoolExecutor 的任务处理流程:1)当前线程数 < corePoolSize → 直接创建新线程执行;2)线程数 ≥ corePoolSize → 放入 workQueue 等待;3)workQueue 已满且线程数 < maximumPoolSize → 创建新线程执行;4)线程数 ≥ maximumPoolSize 且队列满 → 执行拒绝策略(RejectedExecutionHandler)。keepAliveTime 控制非核心线程的空闲存活时间。",
"source": null,
"related": []
},
{
"id": "sc-013",
"type": "single_choice",
"difficulty": 4,
"tags": [
"Java",
"锁机制",
"synchronized",
"锁升级"
],
"question": "Java synchronized 的锁升级过程是怎样的?",
"options": {
"A": "无锁 → 轻量级锁 → 偏向锁 → 重量级锁",
"B": "偏向锁 → 轻量级锁 → 重量级锁(不可逆)",
"C": "无锁 → 偏向锁 → 轻量级锁 → 重量级锁",
"D": "轻量级锁 → 重量级锁 → 偏向锁(竞争结束后降级)"
},
"answer": "C",
"explanation": "JVM 对 synchronized 的优化采用逐步升级策略:1)无锁状态 → 首次进入同步块时,如果对象头 Mark Word 未偏向任何线程,升级为偏向锁(仅记录线程 ID,无 CAS 开销);2)当第二个线程尝试竞争时,偏向锁撤销,升级为轻量级锁(通过 CAS 将 Mark Word 替换为指向栈帧中 Lock Record 的指针);3)轻量级锁竞争失败(自旋超过一定次数),膨胀为重量级锁(OS mutex,未获锁的线程阻塞挂起)。注意:偏向锁在 JDK 15 后默认关闭,因为其撤销成本较高。",
"source": null,
"related": []
},
{
"id": "sc-014",
"type": "single_choice",
"difficulty": 4,
"tags": [
"Java",
"JMM",
"volatile",
"happens-before"
],
"question": "在 Java 内存模型(JMM)中,volatile 变量的 happens-before 语义是?",
"options": {
"A": "对 volatile 变量的写操作 happens-before 后续对同一变量的读操作",
"B": "volatile 仅保证变量的原子性,不保证可见性",
"C": "volatile 读写操作不会插入内存屏障",
"D": "volatile 变量的写 happens-before 所有后续的读写操作"
},
"answer": "A",
"explanation": "根据 JMM 的 happens-before 八大规则之一:volatile 变量规则——对 volatile 字段的写操作 happens-before 后续对同一字段的读操作。具体实现上,volatile 写后插入 StoreStore + StoreLoad 屏障,volatile 读前插入 LoadLoad + LoadStore 屏障。选项 B 错误,volatile 不保证原子性(如 i++ 不是原子操作),但保证可见性。选项 C 错误,volatile 正是通过内存屏障实现的。选项 D 错误,happens-before 是针对同一变量的特定读写对,不是全局的。",
"source": null,
"related": []
},
{
"id": "sc-015",
"type": "single_choice",
"difficulty": 2,
"tags": [
"Java",
"ForkJoinPool",
"工作窃取"
],
"question": "Java ForkJoinPool 的工作窃取(work-stealing)机制是指?",
"options": {
"A": "空闲线程从全局任务队列中取任务执行",
"B": "空闲线程从其他忙碌线程的本地任务队列尾部窃取任务执行",
"C": "主线程将任务平均分配给所有工作线程",
"D": "工作线程按固定顺序依次执行所有提交的任务"
},
"answer": "B",
"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 的等待队列。"
}
]
}