da9667c265
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
512 lines
33 KiB
JSON
512 lines
33 KiB
JSON
{
|
||
"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 的等待队列。"
|
||
}
|
||
]
|
||
} |