什么是协程
协程是一种可以中途暂停、之后再从暂停的地方继续执行的函数。围绕这个概念,需要讲清三件事:它和普通函数、线程到底差在哪,它为什么能撑起几十万并发连接,以及实际使用中最容易踩的坑。

协程的本质是一个能保存执行现场、也能恢复现场的函数。它改变的不是某段代码的写法,而是"函数一旦开始就必须跑到结尾"这条规则。
普通函数为什么会让 CPU 干等
普通函数被调用后,从头跑到尾,中间不会停,跑完了才把控制权还给调用方。这条规则本身没有问题,问题出在等待上。
函数里一旦遇到等网络、等磁盘、等数据库这类操作,CPU 就只能干等着,什么也做不了。执行流被卡在函数内部,这段等待时间被完全浪费。

协程改掉了哪条规则
协程允许函数执行到一半主动让出控制权,等条件满足了再回来接着跑,而且是从刚才那一行继续,之前的局部变量都还在。
所以协程的本质可以概括为:一个能保存执行现场、也能恢复现场的函数。暂停和恢复是它的两个基本动作。

一个做饭的类比
锅里烧着水。普通函数的做法是一路盯着锅,水不开就什么都干不了;协程的做法是先把水烧上,转头去切菜,切到一半水开了,再回来下面条。

等待的时间就这样被利用起来了。协程做的事情,本质上是把原本只能空耗的等待时间还给了程序。
协程和线程的差别
线程是操作系统调度的执行单元,每个线程有自己的栈,创建、销毁、切换都要进内核,成本不低。
协程跑在用户态,由程序自己的调度器管理,切换只是保存几个寄存器和栈指针,开销低一到两个数量级。

落到规模上:一个线程里跑几千上万个协程是常态,而开几千个线程往往就把内存和调度压垮了。

挂起不等于阻塞
这两个概念容易混。阻塞是线程停在那里等,占着线程不放;挂起是把线程让出来,线程立刻可以去跑别的协程。
数量上的差异很直观:一万个正在阻塞的任务需要一万个线程撑着,一万个挂起的协程可能只需要几个线程。

协作式,而不是抢占式
操作系统可以在任意时刻打断一个线程,但没人能强行打断一个协程,它必须自己主动让出。这就是"协作式"的含义。
- 好处:同一线程上的协程不会在指令中间被随机切换,很多共享数据不用加锁。
- 坏处:一个协程如果死循环不让出,同一个线程上其他协程就全部被饿死。

暂停和恢复是怎么发生的
中间有一个角色叫事件循环,也叫调度器。它维护一个就绪队列,挨个取出协程执行。
协程遇到网络请求这类需要等待的操作,就把自己挂起,同时登记一个完成通知,然后交还控制权。等数据到了,通知触发,这个协程被放回队列,从挂起点恢复执行。

await 在代码里意味着什么
写代码的时候,这个让出动作通常就是一个 await。看到 await,可以理解成"这里我要等了,先让出去"。
所以一段带 await 的代码,读起来是自上而下的顺序,实际运行时却被切成很多段,中间穿插着别的协程在执行。

主流语言里的对应实现
- Go:goroutine 配 channel。
- Python:asyncio 和 async/await。
- JavaScript:从 Promise 到 async/await,本质是同一套思路。
- Kotlin:把它做成了语言级特性。

适合与不适合的场景
最典型的是高并发网络服务,比如网关、API 服务、爬虫、即时通讯的长连接。这些任务的共同点是绝大部分时间在等 IO,真正计算的时间很少,协程正好把等待的空档填满。
反过来,视频编码、大矩阵运算这类 CPU 密集型任务,协程帮不上忙,你需要的是多进程和多核并行。

常见误区
一个常见误解是:协程会让程序自动变快,也会自动并行。事实并非如此。
协程提升的是并发能力,也就是同一时间能挂起多少任务,而不是单个任务的执行速度。单线程照样能跑协程,多个协程也不会自动分摊到多个核上。

常见问题
协程和普通函数最根本的区别是什么? 普通函数从头跑到尾、中间不停;协程可以在执行到一半时主动让出控制权,之后再从刚才那一行继续,局部变量还在。
协程和线程谁更轻? 线程由操作系统调度,切换要进内核;协程在用户态由程序自己的调度器管理,切换只保存几个寄存器和栈指针,开销低一到两个数量级。
挂起和阻塞有什么不同? 阻塞是线程停在那里等,占着线程不放;挂起是把线程让出来,线程立刻可以去跑别的协程。一万个阻塞任务需要一万个线程撑着,一万个挂起的协程可能只需要几个线程。
协程能被强行打断吗? 不能。协程是协作式的,必须自己主动让出;一个协程如果死循环不让出,同一个线程上其他协程就全部被饿死。
协程能让程序变快或自动并行吗? 不会。它提升的是并发能力而非单个任务的执行速度,单线程照样能跑协程,多个协程也不会自动分摊到多个核上。
总结
协程用可暂停的函数形态,把等待时间还给了程序;用用户态的轻量调度,替换掉重量级的线程切换;代价是你必须遵守协作规则,在需要等的地方显式让出。

理解这三件事,就能判断什么时候该用协程,什么时候不该用。

