早些年大家聊到 Go,绕不开一句话:goroutine 很轻量,一个协程的栈初始才 2KB,轻轻松松开上百万个。这话本身没错,但“轻量”到底体现在哪些指标上,和操作系统线程比差多少,网上能把底层逻辑和数据说清楚的其实不多。身边同事最常见的误读是“协程就是不用白不用的线程”,结果一上线就被 goroutine 数量拖垮 GC,或者把下游数据库直接打挂。这篇就把线程和协程从调度模型、内存成本、切换开销、实测数据和选型陷阱几个维度拆开聊,适合正在做服务端选型、或者想搞明白 Go 并发模型到底强在哪的读者。
1. 从调度视角看线程与协程的本质差别
1.1 线程是内核管的,协程是运行时管的
线程由操作系统内核直接管理。你每次创建线程,内核要分配线程控制块、内核栈、寄存器上下文,还要把这个线程挂进全局调度队列。线程的创建、唤醒、抢占、销毁,每一步都要经过内核态,最终由内核调度器决定哪个线程跑在哪个 CPU 核上。进程内多线程共享地址空间,但每个线程有独立的寄存器集合和栈,调度器切换线程时,保存和恢复的是一整套执行现场。
Go 协程则完全不同。goroutine 是 Go 运行时自己维护的调度单元,内核完全不感知它的存在。Go runtime 会在进程内部维护若干操作系统线程作为载体,真正放到 CPU 上执行的是这些系统线程。goroutine 本身更像是被扔进“运行队列”里的任务,由 runtime 的调度器决定哪个协程被放到哪个系统线程上执行。用大白话说,线程是内核这个“总公司”的员工,协程是 Go 运行时这个“项目部”自己招的临时工,项目部向总公司租几个正式工名额,临时工们排着队借用这些名额干活。
两者不仅管理方不同,调度时机也不一样。
线程调度大多用的是抢占式调度,内核根据时间片和优先级强制切换。某个线程就算一直霸占 CPU,时间片用完后也得让位,这套机制保证了公平性,代价是每一次切换都要陷入内核。
Go 协程在 1.14 之前是偏协作式的,协程执行到 channel 收发、锁等待、系统调用这些“调度点”时,runtime 才会切入调度器去安排其他协程。从 Go 1.14 开始,runtime 引入了基于信号的异步抢占,长时间运行的协程也会被强制让出,大大改善了公平性。但本质上,Go 调度器还是在用户态完成的,不需要每次切换都进内核,这是性能和开销差异的根源。
1.2 GPM 模型究竟在调度什么
聊 Go 调度绕不开 GPM 这三个缩写。G 是 goroutine,代表一个待执行的用户任务,里面有栈、寄存器上下文的缓存等。P 是 processor,不是 CPU 的意思,而是调度上下文,P 的数量决定了同一时刻最多有多少个 goroutine 处于并行执行状态,默认等于 CPU 逻辑核数。M 是 machine,代表操作系统线程,真实跑在 CPU 上的是 M,而 M 需要绑定一个 P 才能从本地运行队列拿到 G 去执行。
这里给你一个直观对应:如果你的机器是 8 核,GOMAXPROCS 默认 8,那就意味着 runtime 最多同时让 8 个操作系统线程并行工作。你开了 10 万个协程,这 10 万 G 会在 P 的本地队列和全局队列里排队,被不断调度到 8 个 M 上轮流执行。
理解了这层关系,你就明白一个关键点:协程的“轻量”不等于并行能力更强。并行上限还是由 CPU 核数决定的。协程的优势在于你可以在单机上维持海量“就绪状态”的任务,而不是让操作系统去维护海量的内核线程。
1.3 映射模型的差异
线程和协程最终都要落实到“谁跑在 CPU 上”这个问题,区别在于映射方式。
- 1:1 模型:一个用户线程对应一个内核线程。Java 传统线程模型、C++ 的 std::thread 都是这样实现,使用简单,但线程数量和调度开销都受内核限制。
- N:1 模型:多个用户级线程复用一个内核线程。Python 的 asyncio 基本属于这个路线(单线程事件循环),优点是很轻,缺点是一旦某个任务阻塞,整个线程全卡住。
- M:N 模型:M 个用户级线程复用 N 个内核线程。Go 就是这种,兼顾了并发数量和并行能力。
Go 的 runtime 调度器会在以下场景触发调度:创建或销毁 goroutine、channel 收发、time.Sleep、系统调用、锁竞争、GC 开始与结束等。系统调用是特别需要注意的场景,也是 Go M:N 模型设计最巧妙的地方之一。
当一个 M 上的 G 发起阻塞式系统调用时,Go runtime 不会傻等着,而是把这个 M 与对应的 P 解绑,再安排一个新的 M 来接管这个 P 继续执行其他 G。也就是说,一个 goroutine 因为读文件或写网络阻塞时,占用的 P 还能继续服务其他协程。这套机制是 Go 在高并发 IO 场景下表现出色的底层支撑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 内存占用与创建开销:百万级并发的硬件账
2.1 初始栈大小差了四个数量级
线程创建时,内核会为它
