异步编程:从异步I/O模型到协程
最近准备用 FastAPI 写毕设工作的后端,之所以选他是因为自带swagger 支持,也有类型系统,之前写过一些,体验还不错。今天在翻看 FastAPI 的官方文档时,发现一个将并发讲的有声有色的文档。并发 async / await - FastAPI
JS 中有 async/await, python 中也有,理解不同语言之间面对某些问题的相似性处理,必然有助于进一步理解不同语言处理同样问题的通用解决方案。
Python 的现代版本支持通过一种叫"协程"(
async和await语法)的东西来写”异步代码“。
I/O 操作
广义上的 I/O (Input/Output)指的是计算机系统与外部世界之间的数据交互,就是计算机从外部设备获取数据(输入),或向外部设备发送数据(输出)的过程。
代码中的 I/O 操作就是,程序与外部世界的数据交互,对于程序而言,他的外部世界就包括文件系统、网络、键盘、其他程序等等,下面主要以代码中的 I/O 进行说明。
c = a + b这样的简单任务不是 I/O操作
网络请求就是一个 I/O 操作,当你在代码中发起一个请求,数据需要从内存经过操作系统内核进行数据包的包装和发送,比如经过应用层、传输层、网络层、链路层、物理层等等一步步包装过程最终才得以发出。
可以看到,I/O 操作的两大特征就是 外部数据交互 操作系统内核参与
常见的I/O任务包括:
- 程序通过网络发送数据
- 程序通过网络接受数据
- 程序读取本地文件(本质是让操作系统读取磁盘文件内容再供给程序)
- 程序写入本地文件(同理)
- API 远程调用 (RPC)
- 数据库操作
异步
I/O 操作是程序性能的主要瓶颈之一。
CPU 的运行速度非常快,以纳秒(10−9 秒)为单位。而 I/O 操作,比如从硬盘读取数据,速度则慢得多,可能需要毫秒甚至更长。这种巨大的速度差异意味着,如果程序在执行 I/O 操作时“傻傻地”等待,那么 CPU 就会长时间空闲,导致程序运行效率低下。
为了解决这个问题,编程语言和操作系统提供了多种处理 I/O 的模型:
同步 I/O(Synchronous I/O): 这是最简单的模型。当程序发起一个 I/O 请求(比如
file.read())后,它会暂停执行,直到数据读取完毕后才会继续。异步 I/O(Asynchronous I/O): 这种模型允许程序发起 I/O 请求后立即返回,并继续执行其他任务。当 I/O 操作完成后,系统会通过回调、事件或通知来告诉程序。这在需要处理大量并发连接(如网络服务器)时非常有用。
落实到编程语言的异步代码上,异步代码意味着编程语言可以告诉计算机程序在代码中的某个点,它(程序)不需要等待某个任务(T)完成,可以先做其他事情。当任务 T 完成时,它(程序)再执行依赖 T 执行结果的代码(回调)。
在大多数 Web 应用的场景下,服务器会收到来自不同客户端,不同网络条件下发过来的请求,必须要有这样的异步机制。
async / await
现代版本的 Python 有一种非常直观的方式来定义异步代码。这使它看起来就像正常的"顺序"代码,并在适当的时候"等待",基本和 JS 的 async / await 语法类似。
当有一个操作需要等待才能给出结果,且支持这个新的 Python 特性时,你可以编写如下代码:
burgers = await get_burgers(2)
这里的关键是 await。它告诉 Python 它必须等 ⏸ get_burgers(2) 完成它的工作 🕙 ,然后将结果存储在 burgers 中。这样,Python 就会知道此时它可以去做其他事情 🔀 ⏯ (比如接收另一个请求)。
注意,所谓
await并不是告诉python要一直在那傻傻地等任务完成。而是告诉 python 这部分代码在这里暂停,先去干别的,当get_burgers(2)完成了再回来把结果存在burgers中
要使 await 工作,它必须位于async函数内
async def get_burgers(number: int):
# Do some asynchronous stuff to create the burgers
return burgers使用 async def,Python 就知道在该函数中,它将遇上 await,并且它可以"暂停" ⏸ 执行该函数,直至执行其他操作 🔀 后回来。
类比 JS ,我们就可以猜想,async function对应了 *function,声明了一个generator函数,await 就对应了一个 yield 操作符,控制程序的暂停执行。
协程
协程是异步编程中的关键技术
概念
协程是一种用户态的轻量级线程,它由程序自己调度,不需要操作系统内核接入,这意味着协程的切换、调度非常快。
Async / Await -- 自动化协程
在 JS 中的实现:
- 在 JS 中的表现为一个可以暂停和恢复执行的函数。
generator / yield是协程的实现语法。- async / await 是基于
generator / yield的高级语法糖
为了解决 generator 在处理异步时的麻烦,JavaScript 引入了 async/await。它在底层依然使用了 generator 的机制,但做了两件重要的事情:
自动化执行:
async函数的执行流不再需要你手动调用next()。内置对 Promise 的支持:
await关键字专门用来等待一个 Promise 对象的解决。当await遇到一个 Promise 时,它会自动暂停执行,直到 Promise 得到结果。
async / await 是实现自动化调用和 promise 支持的呢
- 一个 async 函数会变成一个 generator 函数
- await 会变为 yield
- 外层由一个 Promise 包装器控制自动执行
// 原始函数
async function greet() {
const result = await Promise.resolve('Hello');
return result + ' World';
}// 自动执行器,接收一个Generator函数
function asyncToGenerator(generatorFunc) {
return function() {
const generator = generatorFunc.apply(this, arguments);
// 整体返回一个 promise,控制函数的返回值
return new Promise((resolve, reject) => {
// 基于 generator.next 递归调用
function step(nextResult) {
// 递归终止条件,done === true
if (nextResult.done) {
return resolve(nextResult.value);
}
// 包装兼容,处理 yield 普通值或promise的情况
Promise.resolve(nextResult.value).then(
res => {
step(generator.next(res));
},
err => {
step(generator.throw(err));
}
);
}
step(generator.next());
});
};
}
// 被转译后的 greet 函数
const greet = asyncToGenerator(function*() {
const result = yield Promise.resolve('Hello');
return result + ' World';
});