看过小林大佬写的进程线程知识点的都知道进程线程的相关概念(小林真是一个好小林)。那么,Go的协程是什么呢,跟进线程有什么区别和联系吗?
其实,线程分为三种,内核线程、用户线程和轻量级用户态线程。而Go的多协程Goroutine 是其并发模型的核心,通过 轻量级用户态线程 + M:N 调度模型 实现高效并发的。
1. Goroutine 的本质
-
轻量级协程:由 Go 运行时(Runtime)管理,非操作系统线程,创建和切换开销极低(初始仅 2KB 栈,可动态扩容)。
-
M:N 调度模型:
-
M 个 Goroutine 映射到 N 个操作系统线程(内核线程)上运行(默认
N = CPU 核数)。 -
由 Go 调度器(Scheduler) 负责协程的调度,避免内核态切换的开销。
-
2. 核心组件:GMP 调度模型
Go 调度器的核心是 GMP 模型,包含三个关键角色:
| 组件 | 作用 | 数量限制 |
| G (Goroutine) | 用户协程,存储执行栈和状态 | 理论上可创建百万级 |
| M (Machine) | 内核线程(OS Thread),真正执行计算的载体 | 默认最多 10000(可调) |
| P (Processor) | 逻辑处理器,管理本地运行队列(Local Run Queue)和资源 | 默认等于 CPU 核数(GOMAXPROCS) |
工作流程:
-
P 绑定到一个 M 上,从本地队列(LRQ)或全局队列(GRQ)获取 G 执行。
-
若 G 发生阻塞(如系统调用),P 会解绑当前 M,并切换到其他 M 继续执行其他 G。
-
阻塞操作完成后,G 会被重新放入队列等待调度。
3. 协程调度策略
(1) 协作式调度
-
主动让出:Goroutine 在以下情况会主动让出 CPU:
-
执行阻塞操作(如 I/O、
channel通信)。 -
系统调用(如文件读写)。
-
-
非抢占式:默认不依赖时间片强制切换,依赖协程自觉让出。
(2) 抢占式调度(Go 1.14+)
-
时间片强制切换:若 Goroutine 运行超过10ms,会被标记为可抢占,避免长时间占用 CPU。
-
基于信号的抢占:通过
SIGURG信号触发调度器介入。
4. 协程的生命周期
-
创建:通过
go func()创建,初始栈大小为 2KB。 -
执行:被调度器分配到 P 的本地队列,由 M 执行。
-
阻塞/唤醒:
-
若 Goroutine 阻塞(如等待
channel),M 会解绑 P 并挂起,P 转而执行其他 G。 -
当阻塞条件解除(如
channel数据到达),G 被重新放入队列。
-
-
退出:函数执行完毕后,栈内存被回收(可能缓存复用)。
5. 关键优化机制
(1) 工作窃取(Work Stealing)
-
若某个 P 的本地队列为空,会从其他 P 的队列或全局队列“窃取” G,提高 CPU 利用率。
(2) 系统调用优化
-
异步 I/O 集成:通过
netpoll(基于epoll/kqueue)将阻塞 I/O 转为异步事件驱动,减少线程阻塞。 -
线程复用:阻塞的系统调用(如文件读写)会触发 P 和 M 解绑,避免浪费线程资源。
(3) 栈管理
-
动态扩容/缩容:Goroutine 栈初始 2KB,按需自动扩容(最大可达 1GB),避免内存浪费。
-
栈分裂(Stack Splitting):处理栈溢出时,将栈迁移到更大的连续内存空间。
6. 与 Channel 的协同
-
通信即同步:
channel是 Goroutine 间通信的核心机制,发送/接收操作会自动触发协程调度: -
ch := make(chan int) go func() { ch <- 1 }() // 发送协程可能阻塞,让出 CPU val := <-ch // 接收协程可能阻塞,让出 CPU -
阻塞唤醒机制:
channel操作会将 Goroutine 挂起,直到数据就绪后由调度器唤醒。
7. 性能特点
| 场景 | 传统线程(Java/C++) | Goroutine |
| 创建速度 | 慢(需内核介入) | 极快(用户态分配) |
| 切换开销 | 高(需内核切换) | 极低(仅保存寄存器) |
| 内存占用 | 1MB+/线程 | 2KB~1GB/协程(动态调整) |
| 并发规模 | 数千线程 | 百万级协程 |
| 适用场景 | CPU 密集型 | I/O 密集型 + 高并发 |
8. 总结
-
Goroutine 的核心优势:通过 用户态调度 + M:N 模型,实现高并发与低开销的统一。
-
调度器设计:GMP 模型、工作窃取、异步 I/O 集成等机制,确保高效利用 CPU 和 I/O 资源。
-
适用场景:微服务、网络代理、爬虫等高并发 I/O 密集型应用。
那么,Go这么强,并发这么厉害,Java无动于衷等着被淘汰?你看看你看看,越来越多企业开始用Go开发了(就连我一个小小破实习生都从Java实习转到Golang实习了[doge]),Java还不快出手!?
其实,Java对应的也有一个与Go协程类似的手段,那就是JDK21的虚拟线程。
Java 虚拟线程(Virtual Threads)是JDK 21引入的一项并发特性,旨在解决传统线程模型在高并发场景下的性能瓶颈。
1. 虚拟线程的本质
-
轻量级线程:由 JVM 管理,不直接映射到操作系统线程(内核线程),而是通过 M:N 调度模型(多个虚拟线程复用少量平台线程)实现高并发。
-
低资源消耗:每个虚拟线程仅需约 40-50 字节 内存,而传统线程需 1MB+,支持创建百万级线程。
-
透明阻塞处理:当虚拟线程执行 I/O 阻塞操作时,JVM 自动挂起并释放底层线程资源,通过事件驱动机制(如
epoll)在 I/O 完成后恢复执行。
2. 与传统线程的对比
| 特性 | 平台线程(传统线程) | 虚拟线程 |
| 管理方 | 操作系统(内核调度) | JVM(用户态调度) |
| 创建成本 | 高(需内核资源) | 极低(用户态分配) |
| 阻塞影响 | 占用线程导致吞吐量下降 | 自动挂起,不阻塞底层线程 |
| 最大并发数 | 受限于系统线程数(通常数千) | 可达百万级 |
| 适用场景 | CPU 密集型任务 | I/O 密集型任务(如网络请求) |
3. 核心优势
-
高吞吐量:在 I/O 密集型场景(如微服务、Web 服务器)中,性能提升 4-6 倍,内存占用降低 60%+。
-
简化编程模型:开发者无需使用复杂的异步回调(如
CompletableFuture),可直接编写同步代码,由 JVM 自动优化阻塞操作。 -
无缝集成:与现有 Java API(如
ExecutorService、ThreadLocal)兼容,无需重写代码。
4. 使用虚拟线程带来的优化
其实,虚拟线程可以用下面代码来解释。
如下代码,如果我使用一个核心线程数为3的传统线程池,如果前三个future对象都去IO读写文件导致阻塞了,那么第四个future任务就会阻塞等待获取线程。
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 提交 4 个阻塞任务
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> blockingIO(), threadPool);
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> blockingIO(), threadPool);
CompletableFuture<Void> future3 = CompletableFuture.runAsync(() -> blockingIO(), threadPool);
CompletableFuture<Void> future4 = CompletableFuture.runAsync(() -> blockingIO(), threadPool); // 会排队等待
// 合并结果
CompletableFuture.allOf(future1, future2, future3, future4).join();
但是!!如果我们采用虚拟线程池化技术,虚拟线程在阻塞时自动挂起,释放底层线程资源并且运行其他任务,等到原本阻塞的任务执行完毕了再重新调度获取空闲线程去执行,解决了阻塞的问题。
ExecutorService vThreadPool = Executors.newVirtualThreadPerTaskExecutor();
// 提交 4 个阻塞任务(实际仅需少量物理线程)
CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> blockingIO(), vThreadPool);
CompletableFuture<Void> future2 = CompletableFuture.runAsync(() -> blockingIO(), vThreadPool);
CompletableFuture<Void> future3 = CompletableFuture.runAsync(() -> blockingIO(), vThreadPool);
CompletableFuture<Void> future4 = CompletableFuture.runAsync(() -> blockingIO(), vThreadPool);
// 所有任务并发执行,物理线程自动复用
CompletableFuture.allOf(future1, future2, future3, future4).join();
但是,事实上很多公司都停留在较低版本的JDK开发,JDK11和JDK17是目前的主流(还有部分清朝编程需要实习生改JSP代码的我就不点名了),对于线程池阻塞的问题,无法使用虚拟线程特性的都是使用 调大线程池数量 、 不阻塞并设置异步回调函数 之类的方案去解决。对于JDK21版本,目前也只能少量的公司会使用。

被折叠的 条评论
为什么被折叠?



