WHAT - 协程及 JavaScript 具体代码示例
文章目录
- 和线程的区别
- 协程的特点
- 协程+事件循环的调度方式
- 关键点
- 举个例子(直观理解)
- 结论:适合 I/O 密集型任务,不适合 CPU 密集型任务
- 常见语言中的协程基本使用
- Python 和 Go
- JavaScript 更具体的代码示例
- 普通同步写法(阻塞 I/O,串行执行)
- 协程/异步写法(高并发 I/O,几乎同时发出请求)
- 简单总结
协程(Coroutine)是一种比线程更轻量级的并发编程方式。它的核心思想是:在单个线程中,通过挂起与恢复来实现多任务的协作执行。
和线程的区别
- 线程:由操作系统调度,开销大,切换时需要保存上下文,可能涉及内核态和用户态切换。
- 协程:由用户代码控制切换,不需要操作系统参与,切换开销小,通常只是在用户态保存/恢复栈和寄存器。
协程的特点
- 轻量:一个线程可以运行成千上万个协程。
- 非抢占式:协程的切换是显式的(例如
await
/yield
),不会像线程那样随时被抢占。 - 顺序写法、异步执行:代码看起来像同步,但实际上在等待 I/O 时切换到其他任务,提高效率。
- 常用于高并发、I/O 密集型:比如网络请求、数据库操作、文件读写。
协程+事件循环的调度方式
在前面介绍中,直觉上会以为协程像单线程一样“排队执行”,怎么还能支持高并发呢?核心原因在于 协程+事件循环(event loop) 的调度方式。
关键点
-
顺序是单个线程内的顺序
协程本质上还是运行在一个线程里,的确不会同时并行执行。
但是:当一个协程遇到 I/O 操作(比如网络请求、文件读写、数据库查询) 时,它不会傻等,而是“让出控制权”,切到下一个协程继续执行。 -
节省了等待的时间
在传统线程里,一个阻塞的 I/O 操作可能要等几百毫秒甚至几秒,这段时间线程就闲着,浪费资源。
协程则在这段时间里切去运行其他任务,把 CPU 充分利用起来。 -
数量极多
因为协程是用户态的轻量调度,一个协程只占用 KB 级别的内存,成千上万个协程同时存在几乎没什么问题。
相比之下,线程每个可能占 MB 级别内存,操作系统还要做上下文切换,几千个线程就会崩溃。
举个例子(直观理解)
-
线程模型:
你有 10 个服务员(线程),每个人点单后要傻等厨房出菜(阻塞),需要很多服务员才能服务 100 桌客人。 -
协程模型:
你只雇 1 个服务员(单线程),但他点完单就去服务下一桌(切换协程),厨房做好菜再通知他回来送菜。
这样 1 个服务员就能高效服务成百上千桌,因为大部分时间都在等菜,而不是一直闲着。
结论:适合 I/O 密集型任务,不适合 CPU 密集型任务
- 协程 不是真正的并行,它们是 高效的并发。
- 高并发来自于:在 I/O 等待时不浪费时间,能调度成千上万个协程轮流执行。
- 如果是 CPU 密集型任务(比如压缩视频、训练模型),协程就没优势,还是要靠多线程/多进程。
常见语言中的协程基本使用
Python 和 Go
-
Python
import asyncioasync def fetch_data():print("开始获取数据...")await asyncio.sleep(2) # 模拟IO操作print("获取完成")return {"data": 123}async def main():result = await fetch_data()print(result)asyncio.run(main())
-
Go(通过
goroutine
实现,虽然严格说是“协程模型”而不是协程本身)package mainimport ("fmt""time" )func worker() {fmt.Println("开始工作...")time.Sleep(2 * time.Second)fmt.Println("完成工作") }func main() {go worker() // 启动协程time.Sleep(3 * time.Second) }
JavaScript 更具体的代码示例
普通同步写法(阻塞 I/O,串行执行)
function sleep(ms) {return new Promise(resolve => setTimeout(resolve, ms));
}async function fetchData(id) {console.log(`开始获取数据 ${id}`);await sleep(2000); // 模拟网络 I/Oconsole.log(`完成获取数据 ${id}`);return { id, data: 123 };
}async function main() {console.time("total");await fetchData(1);await fetchData(2);await fetchData(3);console.timeEnd("total");
}main();
👉 执行结果:
开始获取数据 1
完成获取数据 1
开始获取数据 2
完成获取数据 2
开始获取数据 3
完成获取数据 3
total: ~6000ms
每次都要等上一个任务完成,总耗时 ≈ 2000ms × 3。
协程/异步写法(高并发 I/O,几乎同时发出请求)
function sleep(ms) {return new Promise(resolve => setTimeout(resolve, ms));
}async function fetchData(id) {console.log(`开始获取数据 ${id}`);await sleep(2000); // 模拟网络 I/Oconsole.log(`完成获取数据 ${id}`);return { id, data: 123 };
}async function main() {console.time("total");const tasks = [fetchData(1), fetchData(2), fetchData(3)];const results = await Promise.all(tasks);console.log(results);console.timeEnd("total");
}main();
👉 执行结果:
开始获取数据 1
开始获取数据 2
开始获取数据 3
完成获取数据 1
完成获取数据 2
完成获取数据 3
total: ~2000ms
- 并没有多线程,还是在一个线程里。
- 但是 3 个请求同时挂起等待 I/O,等待时 CPU 空出来去跑其他协程。
- 总耗时 ≈ 单个请求的耗时(2000ms),而不是叠加。
这就体现了 高并发 I/O 密集型 的优势:协程(async/await)+ 事件循环,使得单线程可以同时处理成百上千个 I/O 请求,而不用为每个请求都开一个线程。
简单总结
- 协程是一种 用户态的轻量线程。
- 主要解决 高并发、I/O密集 的场景问题(结合事件循环进行调度)。
- 用“看似同步,实则异步”的写法提升代码可读性。