介绍
什么是异步编程?
简单来说, 异步编程是一种编程范式, 它允许程序在等待一个耗时操作(如网络请求、文件读写)完成的同时, 继续执行其他任务, 而不是“傻等”着结果回来
- 同步(你做饭,等水开): 把水壶放在灶上, 然后站在那里盯着水壶, 直到水烧开, 你才去切菜。这期间你什么事都做不了。这就是
阻塞 - 异步(你做饭,水开了告诉我):把水壶放在灶上, 按下烧水开关, 然后转身去切菜、洗锅。水烧开后, 水壶发出“哔哔”的提示音(回调), 你放下菜刀去关闭开关。这期间你充分利用了时间。这就是
非阻塞
基础概念
- 并行(parallelism): 同时执行多个操作(比如多线程/多进程)
- 并发(concurrency): 快速切换操作(如: JS 的 Event Loop)
- 串行(Serial work): 任务按照特定顺序, 一个接一个的完成
- 阻塞(Blocking): 阻止程序的后续执行, 直到某个操作完成
- 非阻塞(Non-blocking): 在程序执行时, 如果某个操作没有完成, 就不会阻塞程序后续执行
- 回调函数(Callback): 把
水开后要做什么作为函数传入, 等操作完成时自动调用, 这是最基础的形式, 但容易形成callback hell(层层嵌套, 代码难以维护) - 事件循环(EventLoop): JavaScript 和 Python 的 asyncio 都依赖这个。它像一个一直运行的"调度器", 不断检查是否有“烧开了”的事件, 如果有就把对应的任务丢给CPU去执行
- Promise/Future: 这是一个
承诺, 它代表一个未来才会完成的操作, 你可以先拿到这个凭证, 等结果准备好后, 再通过await去拿实际数据
程序操作类型
- CPU 密集型(CPU-bound): 如导出视频(需要进行大量计算)
- IO 密集型(IO-bound): 如大量网络请求/读写文件
学习资料
异步编程模型
OS 多线程/多进程
- 无需改变编程模型, 线程间同步困难, 性能开销大
- 具体应用: 比如之前写多线程 web 服务器时学过的
线程池
Event-deriven: 事件驱动
- 类似 nodejs 的那种回调函数, 虽然高效, 但由于是非线性控制流, 所以可能会出现
callback hell问题 - 由于是非线性控制的, 所以可能会难以定位和传播错误
- 如 NodeJS 的
fs.readFileAPI
- 类似 nodejs 的那种回调函数, 虽然高效, 但由于是非线性控制流, 所以可能会出现
Coroutine: 微线程(有的也叫
纤程协程)- 类似OS多线程, 但是由程序控制的, 所以没有线程切换的开销, 所以执行效率高, 类似 concurrent
- 一般由编程语言提供的API, 比如: python 的 yield/asyncio, JS 的 Promise API
GMP: Go语言的并发模型
Actor 模型: 暂时还未学习过, 先过
Rust 中的 async/await
多线程与 async/await 的关系
多线程
- 没有额外的运行时, 允许重复利用现有代码,代码几乎无需修改即可获得异步的能力(如之前写多线程web服务器的例子)
- 创建/销毁线程内存和CPU开销大(线程池只能降低一些成本)
async/await
- 内存和CPU开销小, 对比多线程可显著降低内存和CPU开销(大量任务的情况下)
- 需要额外的运行时来生产状态机, 可执行文件大
async/await 这种并发和多线程各有不同的应用场景, 不代表 async/await 就一定比多线程并发更好
就比如你需要锻炼身体, 玩篮球和玩排球的关系, 都可以锻炼身体, 只是玩法不同, 没有哪种更好
语言和库的支持
- rust 标准库只提供了最基本的特性/类型/和功能, 比如
Future trait - rust 编译器直接支持 async/await 语法
- 异步代码/io和任务申城的执行由
async runtimes提供
由于 rust 标准库只提供了最基本的功能和语法支持, 虽然可以实现并发编程, 但是不够方便好用, 所以很多的引用都依赖于社区实现的库, 如: async-std 和 tokio
注意
rust 不允许在 trait 中声明 async 函数
兼容性考虑
async 和同步代码不能总是自由结合(如: 不能直接从同步函数中调用异步函数)
async 代码也不总是能只有结合(如: 因为不同的实现依赖于不同的runtime, 比如 async-std 和 rayon 就不能混着用)