Golang中的协程是什么?大家都说Go支持高并发是为啥?[浅谈轻量级用户态线程]

看过小林大佬写的进程线程知识点的都知道进程线程的相关概念(小林真是一个好小林)。那么,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)

工作流程

  1. P 绑定到一个 M 上,从本地队列(LRQ)或全局队列(GRQ)获取 G 执行。

  2. G 发生阻塞(如系统调用),P 会解绑当前 M,并切换到其他 M 继续执行其他 G

  3. 阻塞操作完成后,G 会被重新放入队列等待调度。


3. 协程调度策略

(1) 协作式调度

  • 主动让出:Goroutine 在以下情况会主动让出 CPU:

    • 执行阻塞操作(如 I/O、channel 通信)。

    • 系统调用(如文件读写)。

  • 非抢占式:默认不依赖时间片强制切换,依赖协程自觉让出。

(2) 抢占式调度(Go 1.14+)

  • 时间片强制切换:若 Goroutine 运行超过10ms,会被标记为可抢占,避免长时间占用 CPU。

  • 基于信号的抢占:通过 SIGURG 信号触发调度器介入。


4. 协程的生命周期

  1. 创建:通过 go func() 创建,初始栈大小为 2KB。

  2. 执行:被调度器分配到 P 的本地队列,由 M 执行。

  3. 阻塞/唤醒

    1. 若 Goroutine 阻塞(如等待 channel),M 会解绑 P 并挂起,P 转而执行其他 G

    2. 当阻塞条件解除(如 channel 数据到达),G 被重新放入队列。

  4. 退出:函数执行完毕后,栈内存被回收(可能缓存复用)。


5. 关键优化机制

(1) 工作窃取(Work Stealing)

  • 若某个 P 的本地队列为空,会从其他 P 的队列或全局队列“窃取” G,提高 CPU 利用率。

(2) 系统调用优化

  • 异步 I/O 集成:通过 netpoll(基于 epoll/kqueue)将阻塞 I/O 转为异步事件驱动,减少线程阻塞。

  • 线程复用:阻塞的系统调用(如文件读写)会触发 PM 解绑,避免浪费线程资源。

(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(如 ExecutorServiceThreadLocal)兼容,无需重写代码。


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版本,目前也只能少量的公司会使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北北牌屈四巴格!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值