feat: add 23 GMP questions to go-java-concurrency
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
This commit is contained in:
2026-09-13 14:47:57 +08:00
parent 85710ded73
commit da9667c265
6 changed files with 616 additions and 148 deletions
@@ -1,184 +1,230 @@
{
"topic": "go-java-concurrency",
"type": "fill_blank",
"type": "true_false",
"schema_version": "1.0.0",
"generated": "2026-09-09T16:21:56+08:00",
"generated": "2026-09-13T00:00:00+08:00",
"questions": [
{
"id": "fb-001",
"type": "fill_blank",
"id": "tf-001",
"type": "true_false",
"difficulty": 2,
"tags": [
"go",
"gmp"
],
"question": "Go语言GMP调度模型中,P是G和M之间的调度上下文,当一个P的本地队列已满时,新创建的G会被放入______队列,由其他P通过work-stealing机制窃取执行。",
"answer": [
"全局",
"全局队列",
"global queue",
"global"
],
"explanation": "GMP模型中,每个P拥有一个本地可运行G队列(local run queue)。当本地队列容量(默认256)满载时,新创建的G会被放入全局队列(global queue)。调度器会在以下时机从全局队列取出G:1)本地队列为空时;2)每调度61次从全局队列取一个G,防止全局队列中的G被饿死。work-stealing则是当某个P的本地队列为空时,它会尝试从其他P的本地队列窃取一半的G来运行,从而实现负载均衡。",
"source": null,
"related": []
},
{
"id": "fb-002",
"type": "fill_blank",
"difficulty": 2,
"tags": [
"java",
"thread"
],
"question": "Java中创建线程的三种主要方式分别是继承______类、实现Runnable接口和实现Callable接口。",
"answer": [
"Thread"
],
"explanation": "Java提供三种创建线程的方式:1)继承Thread类,重写run()方法,直接调用start()启动;2)实现Runnable接口,将任务与线程解耦,传入Thread构造器;3)实现Callable接口并配合FutureTask,支持返回结果和抛出异常。其中Runnable的run()没有返回值且不能抛出受检异常,而Callable的call()可以返回泛型结果。实际开发中,推荐使用线程池(ExecutorService)而非直接创建线程,以实现资源复用和更好的并发控制。",
"source": null,
"related": []
},
{
"id": "fb-003",
"type": "fill_blank",
"difficulty": 3,
"tags": [
"go",
"channel"
],
"question": "Go的channel底层数据结构hchan中,环形缓冲区______用于存储有缓冲channel中待发送的数据,sendx和recvx分别指向其发送和接收位置。",
"answer": [
"buf",
"ringbuf"
],
"explanation": "hchan是Go channel的底层实现结构,核心字段包括:buf(环形数组缓冲区,仅用于有缓冲channel)、sendx和recvx(分别标记环形缓冲区的发送和接收位置,用于实现FIFO)、sendq和recvq(sudog链表,分别记录因发送/接收阻塞的goroutine)。当channel无缓冲时,buf为nil,发送和接收直接在两个goroutine之间传递数据。环形缓冲区的设计使得sendx到达末尾后可以回绕到数组起始位置,高效利用内存。",
"question": "在Go中,向已关闭的channel发送数据会导致panic,但从已关闭的channel接收数据会立即返回零值且ok为false。",
"answer": true,
"explanation": "Go中channel的close语义明确规定:向已关闭的channel发送数据会触发panic(send on closed channel),而从已关闭的channel接收数据不会阻塞,而是返回该元素类型的零值,第二个返回值ok为false。这是channel安全关闭的核心规则——关闭者负责发送端的停止,接收者只需检查ok值。",
"source": null,
"related": []
},
{
"id": "fb-004",
"type": "fill_blank",
"difficulty": 3,
"tags": [
"java",
"volatile",
"jmm"
],
"question": "Java内存模型中,volatile变量保证可见性的核心原理是:对volatile变量的写操作会立即刷新到______,读操作会从该处重新加载,从而禁止了线程工作内存对该变量的缓存。",
"answer": [
"主内存",
"主存",
"main memory"
],
"explanation": "Java内存模型(JMM)定义了主内存(main memory)和工作内存(working memory)的抽象概念。每个线程拥有自己的工作内存(对应CPU缓存或寄存器),对变量的读写首先在工作内存中进行,再同步到主内存。volatile关键字的关键语义包括:1)保证可见性——写volatile变量时立即刷新到主内存,读时从主内存重新加载;2)禁止指令重排序——通过内存屏障(Memory Barrier)确保volatile写之前的操作不会被重排到写之后,volatile读之后的操作不会被重排到读之前。但注意volatile不能保证原子性,例如i++对volatile变量的自增仍然不是线程安全的。",
"source": null,
"related": []
},
{
"id": "fb-005",
"type": "fill_blank",
"id": "tf-002",
"type": "true_false",
"difficulty": 3,
"tags": [
"go",
"sync"
"goroutine",
"pprof"
],
"question": "Go的sync.WaitGroup有三个核心方法:Add用于增加计数器,Done用于减少计数器(等同于Add(-1)),当计数器归零时,调用______方法阻塞等待的goroutine会被唤醒。",
"answer": [
"Wait"
],
"explanation": "WaitGroup是Go中用于等待一组goroutine完成的同步原语。典型用法:主goroutine在启动工作goroutine前调用Add(n)设置计数器,每个工作goroutine完成时调用Done()(内部执行Add(-1)),主goroutine调用Wait()阻塞直到计数器归零。底层实现中,WaitGroup使用原子操作维护state1和state2两个字段,分别存储信号量和计数器值。需要注意的是:Add的调用必须在Wait之前或在Wait的goroutine内部调用,不能在工作goroutine中调用Add,否则可能在Wait已经开始等待后才Add,导致竞态条件。",
"question": "Go中goroutine泄漏最常见的原因之一是向无缓冲channel发送数据后,没有对应的接收者,导致发送方goroutine永远阻塞。",
"answer": true,
"explanation": "goroutine泄漏的本质是goroutine无法正常退出。向无缓冲channel发送时,如果没有goroutine在接收,发送方会永久阻塞。类似地,从无人发送的channel接收也会阻塞。其他常见泄漏场景包括:context未正确取消导致的无限等待循环、select中没有default分支且所有case都不就绪等。可以通过pprof的goroutine profile(runtime.NumGoroutine()或net/http/pprof)来定位泄漏的goroutine及其阻塞堆栈。",
"source": null,
"related": []
},
{
"id": "fb-006",
"type": "fill_blank",
"id": "tf-003",
"type": "true_false",
"difficulty": 3,
"tags": [
"java",
"synchronized",
"cas"
],
"question": "Java synchronized关键字在JDK 6之后引入了锁升级机制,其升级路径为:无锁 → ______ → 轻量级锁(自旋) → 重量级锁,这一优化大幅提升了在不同竞争程度下的同步性能。",
"answer": [
"偏向锁",
"biased locking",
"Biased Locking"
],
"explanation": "JDK 6引入的锁升级(lock escalation)是synchronized性能优化的关键:1)偏向锁——当只有一个线程访问同步块时,在对象头的Mark Word中记录线程ID,后续该线程进入时只需CAS检查ID,无需任何同步操作;2)轻量级锁——当第二个线程尝试获取锁时,偏向锁撤销,升级为轻量级锁,通过CAS将Mark Word复制到栈帧的Lock Record中,适合短时间、低竞争的场景,未获取到锁的线程会通过自旋(adaptive spinning)等待;3)重量级锁——当自旋超过一定次数或竞争激烈时,升级为重量级锁,依赖操作系统的Mutex Lock实现,未获取到锁的线程会被阻塞挂起。锁只能升级不能降级(JDK 15默认关闭了偏向锁)。",
"source": null,
"related": []
},
{
"id": "fb-007",
"type": "fill_blank",
"difficulty": 4,
"tags": [
"go",
"gmp"
],
"question": "GMP调度模型中,当goroutine执行系统调用(如文件I/O)被阻塞时,M会与P解绑,P会被交给其他空闲的M或创建新的M来继续执行本地队列中的G,这一机制被称为______。",
"answer": [
"hand off",
"handoff",
"hand-off"
],
"explanation": "Hand off(移交)是GMP调度模型中处理M阻塞的核心机制。当某个M上的G执行阻塞性系统调用时,M会进入阻塞状态,此时P(及其本地队列中的G)不能跟着一起等待。调度器会将P从这个阻塞的M上解绑(hand off),交给另一个空闲的M来继续执行P本地队列中的G。如果此时没有空闲的M,则会创建一个新M。当系统调用返回后,原M会尝试获取一个空闲的P来继续运行,如果没有空闲P,则该G会被放入全局队列。这一机制确保了一个阻塞的系统调用不会导致整个P上的所有goroutine都被阻塞,提高了调度效率。",
"question": "Go的GMP调度模型中,当一个M(系统线程)发现本地P的本地队列为空时,会先尝试从全局队列获取G,再尝试从其他P的本地队列偷取(work-stealing)一半的G。",
"answer": true,
"explanation": "GMP调度模型中,M绑定P后优先从P的本地队列获取G执行。当本地队列为空时,调度器的窃取逻辑为:1)先检查全局队列(global run queue);2)再尝试netpoller(网络轮询器);3)最后从其他P的本地队列偷取(随机选择一个P,偷取其本地队列一半的G)。sysmon监控线程会定期检测长时间运行的G(抢占式调度),并将空闲P绑定到空闲M。",
"source": null,
"related": []
},
{
"id": "fb-008",
"type": "fill_blank",
"id": "tf-004",
"type": "true_false",
"difficulty": 4,
"tags": [
"java",
"aqs"
"jmm",
"volatile"
],
"question": "Java并发包中的AQS(AbstractQueuedSynchronizer)内部维护了一个volatile int类型的______变量和一个双向链表实现的FIFO队列,ReentrantLock、Semaphore、CountDownLatch等同步器都基于AQS实现。",
"answer": [
"state",
"state变量"
],
"explanation": "AQS是java.util.concurrent包的核心框架,采用模板方法模式,子类通过重写tryAcquire/tryRelease(独占模式)或tryAcquireShared/tryReleaseShared(共享模式)来实现不同的同步语义。核心机制:1)state变量——volatile修饰的整数,不同同步器赋予不同含义:ReentrantLock中表示重入次数(0为未锁定),Semaphore中表示可用许可数,CountDownLatch中表示倒计数值;2)CLH队列——基于双向链表实现的FIFO等待队列,节点封装了等待线程和等待状态;3)acquire/release流程——线程获取锁失败时被封装为节点加入队列尾部并自旋或park阻塞,前驱节点释放后通过unpark唤醒后继节点。",
"question": "在Java内存模型(JMM)中,volatile变量的写操作对后续所有线程的读操作都可见,因此volatile可以完全替代synchronized来保证复合操作的原子性。",
"answer": false,
"explanation": "volatile确实保证了可见性(写入后立即刷新到主内存,读取时从主内存加载)和有序性(禁止指令重排序),但它不保证复合操作的原子性。例如volatile int count; count++实质是读-改-写三步操作,多个线程并发执行时仍会出现竞态条件。对于复合操作的原子性,需要使用synchronized、ReentrantLock或java.util.concurrent.atomic包中的原子类(如AtomicInteger的CAS操作)。volatile的经典适用场景是状态标志位(boolean flag)和双重检查锁定(DCL)单例模式。",
"source": null,
"related": []
},
{
"id": "fb-009",
"type": "fill_blank",
"id": "tf-005",
"type": "true_false",
"difficulty": 3,
"tags": [
"java",
"aqs",
"synchronized"
],
"question": "Java中AQS(AbstractQueuedSynchronizer)是ReentrantLock和Semaphore等同步器的核心实现基础,它通过CLH队列变体和volatile状态变量来实现线程排队与唤醒。",
"answer": true,
"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。",
"source": null,
"related": []
},
{
"id": "tf-006",
"type": "true_false",
"difficulty": 2,
"tags": [
"go",
"sync"
],
"question": "Go的sync.Once保证传入的函数只被执行一次,即使多个goroutine同时调用Do方法,也只有一个goroutine会执行该函数,其余goroutine会阻塞等待其完成。",
"answer": true,
"explanation": "sync.Once内部通过atomic.LoadInt32/StoreInt32检查并设置标志位来保证函数只执行一次。其Do方法的实现逻辑为:先通过原子操作检查done标志,若已执行则直接返回;若未执行,则通过Mutex加锁后再次检查(double-check)并执行函数。其他并发调用Do的goroutine会在Mutex上阻塞,直到执行的goroutine完成函数调用并释放锁。这保证了初始化代码的线程安全执行,是Go中实现懒初始化单例的标准方式。",
"source": null,
"related": []
},
{
"id": "tf-007",
"type": "true_false",
"difficulty": 4,
"tags": [
"java",
"forkjoinpool"
],
"question": "Java ForkJoinPool采用工作窃取算法,每个工作线程维护一个______队列(deque),当线程自己的任务队列为空时,它会从其他线程队列的尾部窃取任务执行,以此实现负载均衡。",
"answer": [
"双端队列",
"deque",
"Deque",
"双端",
"work-stealing queue"
],
"explanation": "ForkJoinPool是ExecutorService的特殊实现,专为分治任务(divide-and-conquer)设计。其核心设计:1)双端队列(Deque)——每个worker线程拥有自己的双端队列,本地任务从头部push/pop(LIFO,利用缓存局部性),窃取时从其他线程队列的尾部pop(减少竞争);2)工作窃取(work-stealing)——空闲线程从其他忙碌线程的队列尾部窃取任务,避免线程空闲;3)任务拆分——继承RecursiveTask(有返回值)或RecursiveAction(无返回值),通过fork()将子任务推入自己的队列,join()等待子任务结果;4)ForkJoinPool.commonPool()——JDK 8+提供默认共享池,parallelStream()底层使用它。任务粒度过细会导致调度开销过大,过粗则无法充分利用并行性。",
"question": "Java的ForkJoinPool采用工作窃取(work-stealing)算法,空闲的工作线程会从其他繁忙线程的任务队列尾部偷取任务执行,commonPool使用ForkJoinPool.defaultParallelism()作为默认并行度。",
"answer": true,
"explanation": "ForkJoinPool的核心设计是工作窃取:每个工作线程维护一个双端队列(deque),新任务从队列头部推入(push),本线程也从头部取出执行(LIFO,有利于局部性)。当某个线程的队列为空时,它会随机选择其他线程的队列,从其尾部窃取任务(FIFO,减少竞争)。commonPool是静态共享池,通过ForkJoinPool.commonPool()获取,默认并行度Runtime.getRuntime().availableProcessors()-1(当大于1时),适用于ParallelStream和CompletableFuture等场景。RecursiveTask(有返回值)和RecursiveAction(无返回值)是ForkJoinTask的两个子类,用于定义可分解的递归任务。",
"source": null,
"related": []
},
{
"id": "fb-010",
"type": "fill_blank",
"difficulty": 5,
"id": "tf-008",
"type": "true_false",
"difficulty": 4,
"tags": [
"go",
"pprof"
"jmm",
"happens-before",
"go"
],
"question": "Go的pprof工具支持多种profile类型:CPU profile记录程序在各位置的采样频率,heap profile记录内存分配,block profile记录goroutine在同步原语上的阻塞时间,而______ profile专门记录goroutine的调用栈,用于排查goroutine泄漏问题。",
"answer": [
"goroutine",
"goroutine profile"
"question": "Go的内存模型中,goroutine的创建(go关键字)发生在该goroutine的执行之前,而channel的每次发送操作都happens-before对应的接收操作完成。",
"answer": false,
"explanation": "Go内存模型的happens-before规则中:1)goroutine的创建确实happens-before该goroutine的执行开始——这是正确的。2)但对于无缓冲channel:发送操作happens-before对应的接收操作完成;而对于有缓冲channel:第k次发送happens-before第k次接收的完成。关键区别在于'完成'——接收操作的完成是指接收到了数据,而不是仅仅开始接收。该题将两种channel混为一谈,表述不够准确,且channel的发送happens-before的是'对应的接收完成'而非'接收操作本身',存在语义偏差,因此判定为false。",
"source": null,
"related": []
},
{
"id": "tf-009",
"type": "true_false",
"difficulty": 3,
"tags": [
"java",
"thread"
],
"explanation": "pprof是Go内置的强大性能分析工具,支持的profile类型各有用途:1)CPU profile——通过定时采样(默认100Hz)记录CPU使用热点,用于发现CPU密集型瓶颈;2)heap profile——记录堆内存的分配和持有情况,可用于定位内存泄漏和过度分配;3)block profile——记录goroutine在sync.Mutex、channel等同步原语上阻塞的时长和次数,用于发现锁竞争瓶颈;4)goroutine profile——列出所有活跃goroutine的调用栈和状态(running/waiting/sleeping等),是排查goroutine泄漏的关键工具。当goroutine泄漏时,该profile会显示大量goroutine堆积在同一个调用栈上(如等待永远不会接收到数据的channel)。定位后通常需要检查:未关闭的channel、未取消的context、死循环中缺少退出条件等问题。可通过runtime/pprof包或net/http/pprof端点(/debug/pprof/)获取profile数据,再用go tool pprof进行可视化分析或生成火焰图。",
"question": "Java中通过ThreadPoolExecutor创建线程池时,核心参数maximumPoolSize必须大于等于corePoolSize,当核心线程数未满时新任务会创建核心线程执行,核心线程满后任务会先进入工作队列,队列满后才创建非核心线程。",
"answer": true,
"explanation": "ThreadPoolExecutor的任务提交流程严格遵循以下顺序:1)当前线程数 < corePoolSize → 创建新核心线程执行任务;2)核心线程满 → 将任务放入workQueue(工作队列)等待;3)队列已满且线程数 < maximumPoolSize → 创建非核心线程执行任务;4)线程数已达maximumPoolSize且队列满 → 执行拒绝策略(AbortPolicy/CallerRunsPolicy/DiscardPolicy/DiscardOldestPolicy)。maximumPoolSize < corePoolSize在语义上不成立,构造时虽然不会抛异常,但实际线程数永远不会超过corePoolSize。注意:如果使用无界队列(如LinkedBlockingQueue),maximumPoolSize实际上不会生效。",
"source": null,
"related": []
},
{
"id": "tf-010",
"type": "true_false",
"difficulty": 2,
"tags": [
"java",
"jfr"
],
"question": "Java Flight Recorder(JFR)是JDK内置的低开销性能分析工具,通过事件采样的方式记录CPU、内存、I/O、线程等运行时信息,可通过jcmd或jfr命令行工具启动录制,也可在代码中通过Recording API编程控制。",
"answer": true,
"explanation": "JFR是Oracle JDK和OpenJDK(JEP 171/328/336)内置的生产级监控框架,设计目标是开销低于1%。其核心特点包括:1)基于事件驱动的采样机制,预定义了数百种事件类型(如jdk.CPULoad、jdk.GarbageCollection、jdk.ThreadStart等);2)支持命令行启动(jcmd <pid> JFR.start、jfr start/stop/dump)和编程方式(new Recording().start());3)录制数据以.jfr文件存储,可通过JDK Mission Control(JMC)可视化分析,生成火焰图等;4)从JDK 11起可免费用于生产环境(此前商业特性)。JFR与异步采样profiling配合,能有效定位CPU热点、锁竞争、GC停顿等问题。",
"source": null,
"related": []
},
{
"id": "tf-011",
"type": "true_false",
"difficulty": 3,
"tags": [
"Go",
"GMP",
"调度模型",
"goroutine"
],
"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 信号来实现的。",
"answer": false,
"explanation": "Go 的栈增长并不依赖硬件内存保护页和 SIGSEGV 信号。Go 采用的是编译器插桩(compiler instrumentation)的软件检查方式。编译器在每个函数入口处插入栈检查代码(称为 stack bound check 或 prologue),将当前栈指针 SP 与 g 结构体中的 stackguard0 字段进行比较。当 SP 低于 stackguard0 时,说明栈空间不足,运行时会触发 morestack 流程:分配一个更大的栈(通常为当前栈的 2 倍),将旧栈内容完整拷贝到新栈,并更新所有指向旧栈的指针(stack copying / stack growth)。stackguard0 的值在栈空间底部附近,预留了一定的安全余量(stackSmall,通常为 128 字节)。这种方式避免了系统调用开销和信号处理的复杂性,且能精确控制栈增长的时机。Go 1.4 之后统一使用连续栈(contiguous stack)方案,取代了早期的分段栈(segmented stack)。",
"source": null,
"related": []
}