"explanation":"Go中channel的close语义明确规定:向已关闭的channel发送数据会触发panic(send on closed channel),而从已关闭的channel接收数据不会阻塞,而是返回该元素类型的零值,第二个返回值ok为false。这是channel安全关闭的核心规则——关闭者负责发送端的停止,接收者只需检查ok值。",
"explanation":"GMP调度模型中,M绑定P后优先从P的本地队列获取G执行。当本地队列为空时,调度器的窃取逻辑为:1)先检查全局队列(global run queue);2)再尝试netpoller(网络轮询器);3)最后从其他P的本地队列偷取(随机选择一个P,偷取其本地队列一半的G)。sysmon监控线程会定期检测长时间运行的G(抢占式调度),并将空闲P绑定到空闲M。",
"explanation":"volatile确实保证了可见性(写入后立即刷新到主内存,读取时从主内存加载)和有序性(禁止指令重排序),但它不保证复合操作的原子性。例如volatile int count; count++实质是读-改-写三步操作,多个线程并发执行时仍会出现竞态条件。对于复合操作的原子性,需要使用synchronized、ReentrantLock或java.util.concurrent.atomic包中的原子类(如AtomicInteger的CAS操作)。volatile的经典适用场景是状态标志位(boolean flag)和双重检查锁定(DCL)单例模式。",
"explanation":"AQS是java.util.concurrent包的基石,其核心机制为:1)内部维护一个volatile int state变量表示同步状态;2)使用CLH(Craig, Landin, and Hagersten)队列变体实现线程排队,每个等待线程被封装为Node节点;3)获取锁失败的线程被包装为Node加入CLH队列尾部,然后通过LockSupport.park()阻塞;4)释放锁时通过unparkSuccessor()唤醒队列中的下一个线程。ReentrantLock通过state表示重入次数,Semaphore通过state表示可用许可数,CountDownLatch通过state表示倒计数值。而synchronized从JVM层面实现,不依赖AQS。",
"question":"在 Go 的 GMP 调度模型中,当一个 G 进入系统调用(_Gsyscall 状态)且该系统调用阻塞时,G 会从 _Gsyscall 直接转换为 _Gwaiting 状态。",
"answer":false,
"explanation":"这是对 G 状态机的常见误解。当 G 执行系统调用时,它处于 _Gsyscall 状态。如果系统调用阻塞,调度器会执行 hand-off 操作——将 P 从当前 M 上摘下并交给另一个空闲 M(或新建 M)以继续运行其他 G。阻塞的系统调用完成后,G 的状态转换路径是 _Gsyscall → _Grunnable(可运行),而非 _Gwaiting。_Gwaiting 状态用于显式的等待操作,例如 goroutine 阻塞在 channel 接收/发送、sync.Mutex.Lock()、select 无就绪 case 等场景。_Gsyscall 和 _Gwaiting 是两种不同的阻塞语义:前者是操作系统层面的系统调用阻塞,后者是 Go 运行时层面的同步原语阻塞。理解这一区别对于分析 goroutine 的生命周期和 pprof 输出至关重要。",
"source":null,
"related":[]
},
{
"id":"tf-012",
"type":"true_false",
"difficulty":2,
"tags":[
"Go",
"GMP",
"调度模型",
"goroutine"
],
"question":"从 Go 1.14 版本开始,Go 运行时引入了基于信号的异步抢占机制,使用的信号是 SIGURG。",
"answer":true,
"explanation":"Go 1.14 正式引入了基于信号的异步抢占机制(asynchronous preemption),解决了此前协作式抢占(cooperative preemption)的致命缺陷——当 goroutine 没有函数调用(例如执行紧密的纯计算循环)时,调度器无法插入抢占检查点,导致其他 goroutine 被饿死甚至 GC 无法 STW。异步抢占通过向目标 M 发送 SIGURG 信号实现:运行时的 sysmon 后台线程检测到长时间运行的 G 后,向其绑定的 M 发送 SIGURG,M 的信号处理器会在安全点(safe point)暂停当前 G 的执行,将其状态设为 _Grunnable 并放回队列。选择 SIGURG 是因为它在 Linux 上未被标准工具和应用广泛使用,避免与用户信号处理冲突。该机制标志着 Go 从纯协作式调度迈向了混合式调度模型。",
"source":null,
"related":[]
},
{
"id":"tf-013",
"type":"true_false",
"difficulty":2,
"tags":[
"Go",
"GMP",
"调度模型",
"work-stealing"
],
"question":"在 Go 的 GMP 模型中,每个 P(Processor)持有的本地运行队列(local run queue)的固定容量为 256 个 G。",
"answer":true,
"explanation":"Go 运行时中,每个 P 维护一个本地运行队列(local run queue),其实现是一个固定大小为 256 的环形队列(ring buffer)。这个容量在运行时源码中定义为 localRunQueueCap = 256。当一个 P 的本地队列已满时,新创建的 G 或从全局队列获取的 G 不会被放入本地队列,而是会将本地队列中约一半的 G(128个)连同新 G 一起移到全局运行队列(global run queue)中,这一操作称为 runqputslow。这种设计在保证本地队列访问无锁(lock-free)的同时,通过溢出机制避免 G 被丢弃。256 的容量是一个工程权衡——足够大以减少全局队列竞争,又足够小以保持缓存友好性。全局队列则是一个无容量限制的链表结构,使用互斥锁保护。",
"source":null,
"related":[]
},
{
"id":"tf-014",
"type":"true_false",
"difficulty":3,
"tags":[
"Go",
"GMP",
"调度模型",
"goroutine"
],
"question":"在 Go 的 GMP 模型中,当一个 M 进入系统调用时,调度器会立即终止该 M 并将其资源回收,以节省操作系统线程资源。",
"answer":false,
"explanation":"这是错误的理解。当 M 进入系统调用时,Go 调度器执行的是 hand-off(移交)操作,而非终止 M。具体流程如下:M 进入系统调用前,调度器将其绑定的 P 摘下(hand-off),将 P 转交给一个空闲的 M(如果有)或新建一个 M 来接管该 P,确保 P 能继续执行其本地队列中的其他 G。进入系统调用的 M 本身会阻塞在操作系统层面,等待系统调用返回。系统调用完成后,M 尝试获取一个空闲的 P:如果成功则继续运行;如果没有空闲 P,M 会将当前 G 放入全局队列,然后自身进入休眠状态(放入空闲 M 链表),而非被终止。这种设计保证了 P 资源不被浪费,同时 M 可以被复用。M 的数量可能超过 P 的数量(M:N 模型中 N 可大于 M),但受到信号处理等约束限制。",
"source":null,
"related":[]
},
{
"id":"tf-015",
"type":"true_false",
"difficulty":4,
"tags":[
"Go",
"GMP",
"调度模型",
"goroutine"
],
"question":"在 Go 运行时中,栈增长的触发机制是通过硬件内存保护页(guard page)触发 SIGSEGV 信号来实现的。",