Goroutine 像高速公路上成千上万辆货车一样高效运作的轻量级并发模型
1、为什么需要Goroutine(协程)?
现代程序中,尤其是多核CPU下,常常需要同时做很多事,比如:
- 公众号服务器同时处理上万个甚至百万请求
- 搜索引擎的爬虫程序同时抓取多个网站
- 短视频服务器的视频转码时同时压缩多个片段
- 吃鸡游戏服务器同时计算多个玩家的行为
在传统语言(如C++、Java)中,要实现“并发”,需要用到线程(Thread),但是线程的代价高,每个线程都需要:
- 几MB的栈空间
- 操作系统参与调度(频繁上下文切换)
- 创建、销毁核切换相对耗时 当线程一多(例如上万个),程序性能就会急剧下降。
2、协程是什么?
协程是Go语言在语言层面提供的一种轻量级并发单元,它由Go runtime 运行时直接管理,而不是系统直接管理。
可以这样去理解:
“在Go世界里,协程Goroutine就像是一辆小货车,可以轻松跑在Go的高速公路(Thread线程)上执行任务”
3、举个例子🌰:高速公路运输货物
物流公司每天都要运输各种各样的货物到全国各地:
3.1、传统线程模式(每个任务一辆车+一条路)
每运输一批货物,就得:
- 新修一条高速
- 安装收费站和信号灯
- 派一辆车上去跑
显而易见,这样操作的问题是:
- 修路成本太高
- 管理复杂
- 一旦车多,高速就堵死了
Go的调度器更聪明:
- 先修几条高速
- 把上千辆车安排到这些高速上
- 有Go的调度员(Scheduler)自动分派每辆车在哪条高速上跑
- 如果有车慢了、堵了,调度员会把别的车安排到更宽松的车道上
有了这个操作后:
- 能轻松让10万辆货车同时运输
- 每辆车启动几乎不耗时
调度员会智能安排,一切就高效地并行起来了
4、协程和线程的底层比较
| 特性 | 协程 | 传统线程 |
|---|---|---|
| 栈空间 | 初始仅约2KB,可动态增长 | 固定1~8MB |
| 调度者 | Go运行时(runtime) | 操作系统内核 |
| 切换成本 | 运行时上下文切换,很快 | 内核级切换,比较慢 |
| 数量级 | 轻松上万个 | 一般不超过几千个 |
| 创建方式 | 使用go关键字 | 调用复杂的API |
这也是为什么Go在高并发服务器(HTTP服务、微服务、爬虫、分布式系统)中表现优异的原因。
5、实战时间:多辆货车并发送货
package main
import (
"fmt"
"time"
)
func deliverGoods(truck string) {
for i := 1; i <= 3; i++ {
fmt.Printf("%s 正在运送第 %d 批货物\n", truck, i)
time.Sleep(time.Second)
}
}
func main() {
// 启动三辆货车并发运货
go deliverGoods("🚚 卡车A")
go deliverGoods("🚛 卡车B")
go deliverGoods("🚐 卡车C")
fmt.Println("所有货车已出发!")
time.Sleep(5 * time.Second) // 等待所有货车运完
}
运行如图所示:
6、重点:Go的调度机制(GMP模型)
Go的并发调度器采用GMP模型,即:
- G(Goroutine):任务单位(函数)
- M(Machine):操作系统线程
- P(Processor):逻辑处理器,负责调度G到M上运行 简单来说:Go程序启动后,会创建若干P(相当于高速上的车道),每个P维护一个任务队列(待运行的Goroutine),M负责从P的队列里取出并运行任务。当某一辆货车被堵,调度器(天眼)会让别的车继续跑,不浪费时间。
其实,Goroutine 的世界就像我们的人生。 每个 Goroutine 都在执行自己的任务、奔赴自己的目标,有的忙碌运货(工作),有的在等待信号(机会),有的在休眠(休息)。在宏大的系统中,我们每个人看似独立,但都被同一个“调度器”——时间——所管理。有时我们并行前进,有时我们等待他人完成;有时系统为我们分配更多资源,有时我们要让出车道。可无论如何,整个程序都因为每一个小小的 Goroutine 的努力而继续运行。
就像人生:你不需要掌控全部线程,只要让自己的那条 Goroutine 跑得稳、跑得好。