先看日常用法:两件互不依赖的事可以同时推进
用户注册后,系统既要生成欢迎资料,又要发送邮件。两件事互不依赖,可以各自放进一条 goroutine。下面的 make 是创建并初始化特定 Go 数据结构的内置函数,chan 是声明 Channel(通道)的关键字;results 是容量为 2 的“完成结果通道”,taskName 是主流程当前收到的任务名称。
results := make(chan string, 2) go func() { generateWelcomeFile() results <- "欢迎资料" }() go func() { sendEmail() results <- "邮件" }() for i := 0; i < 2; i++ { taskName := <-results fmt.Println(taskName, "完成") }
Channel(通道)是 goroutine 之间传递数据或完成信号的工具。两行输出的先后不保证,但两个任务最终都会完成;这正说明代码描述的是“可以同时推进”,不是规定固定执行顺序。
没有运行时调度会怎样
如果每创建一条 goroutine 就永久创建一条操作系统线程,十万个并发任务会带来大量线程内存、创建销毁和上下文切换成本。上下文切换是处理器暂停当前线程、保存现场,再恢复另一条线程的过程。
业务想要的
可以创建很多轻量任务;某个任务等待网络时,其他任务继续推进。
运行时必须解决的
用有限线程承载大量任务,并决定谁先运行、阻塞后谁顶上、长任务何时让位。
再用厨房建立三个角色的画面
把一个 Go 程序想成餐厅厨房。客人可能瞬间提交几千张订单,但厨房不会为每张订单永久雇一个厨师,而是让有限的厨师和工位不断切换订单。
G:订单
记录“要做什么、做到哪里、当前是什么状态”。订单很多没有关系,因为它本身不是一个真实线程。
M:厨师
真正动手执行。对应操作系统线程,最终由操作系统安排到某个 CPU 上运行。
P:工位
带着待办订单和本地工具。厨师必须占到工位,才有资格执行普通 Go 代码。
并发不等于并行
并发(concurrency)是多个任务在同一时间段内交替推进;并行(parallelism)是多个任务在同一时刻真的一起执行。一个 P 也能并发处理很多 G,但同一时刻只能执行一个普通 Go 任务;多个 P 才可能让多个 G 并行。
1 个 P
G1 做一会儿,阻塞或让位后换 G2。大家都在推进,但普通 Go 代码通常没有同时执行。
4 个 P
最多允许 4 个线程同时执行普通 Go 代码,因此最多有 4 个 G 同时在 CPU 上推进。