学习 pythonweb 框架 fastapi,看到介绍,各种优点 “支持异步,性能好,是最快的 Python 网络框架之一 "
最简单的 FastAPI 像下面这样:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
注意到async def
关键字,如果用过异步的 python 库就很熟悉,这些库都会告诉你在调用时在前面加上关键字await
,但是加上后编辑器就会告诉你要在异步函数中使用,于是在def
前加上async
, 然后把鼠标放在await
后面函数上,发现返回是一个coroutine
的东西,如果像a()
一样直接调用这个异步函数会报错,需要使用asyncio.run()
运行
异步的代码能够告诉程序在其运行的某些时候会等待
(io 操作,网络), 没必要占有 cpu,可以把控制权交给别的任务,等到真的完成后告诉程序运行结果,这样程序中的其他任务不必都等这个慢任务,也就是不和它同步,也就是说同步是按顺序执行,异步是在不同任务之间切换
在 python 中await a()
告诉程序不必等待 a () 的执行,a () 自己或者内部的某个时间可被暂停,等到未来某个时候会有返回结果,在 python 官方文档中 a () 叫做可等待对象
如果一个对象可以在
await
语句中使用,那么它就是 可等待 对象
可等待 对象有三种主要类型: 协程, 任务 和 Future.一个 Future 代表一个异步运算的最终结果。线程不安全。
也就是说 await 后面可以是 coroutine (协程),task (任务),Future
在连续await
时候发现还是同步的,因为 await 后面是 coroutine 时候首先把它变成 task, 这个asyncio.create_task()
是一样的,await 同时干了两件事,导致连续await
任务不是同时创建的,这时候使用asyncio.gather()
并发运行任务
import asyncio
import time
async def f1():
print(f'f1--start-{time.time()}')
await asyncio.sleep(1)
print(f'f1--end-{time.time()}')
async def f2():
print(f'f2--start-{time.time()}')
await asyncio.sleep(1)
print(f'f2--end-{time.time()}')
async def main():
await f1()
await f2()
if __name__ == '__main__':
asyncio.run(main())
f1--start-1681478831.0485213
f1--end-1681478832.0549097
f2--start-1681478832.0549097
f2--end-1681478833.0619018
把 main 函数改成:
async def main():
await asyncio.gather(f1(), f2())
f1--start-1681478900.615097
f2--start-1681478900.615097
f1--end-1681478901.6200216
f2--end-1681478901.6200216
或者这样同时创建任务
async def main():
task1 = asyncio.create_task(f1())
task2 = asyncio.create_task(f2())
await task1
await task2
#或者 await asyncio.gather(task1, task2)
f1--start-1681479408.9956422
f2--start-1681479408.9956422
f1--end-1681479410.0039244
f2--end-1681479410.0039244
await
可以接一个coroutine
或者一个task
,如果是coroutine
,会从它创建一个 task, 如果直接连续 await, 当调用 f1 时,它会立即开始执行,但是在执行过程中,控制权会立即返回到main()
函数中,main()
函数会等待f1
执行完成后才会继续执行下一行代码。然而,在等待f1
执行完成的同时,f2
并没有被调用
如果直接创建 2 个任务没有加await
,main()
函数会在创建任务 task1
和 task2
后立即返回,不会等待这两个任务完成,也就是只有 f1-start 和 f2--start 没有 end
异步有很多优点 -- 可以在一个线程中处理多个任务,不需要为每个任务创建一个新的线程,节省线程切换的开销,提高程序的并发性。可以在等待 IO 操作的同时处理其他任务,不会阻塞程序的执行
异步的意义在于充分利用 cpu,提高程序的效率,因此对于计算密集的程序,异步的意义不大,只会增加复杂性,只有处理大量 IO 操作的场景,如网络编程、Web 开发等才适用
看到异步确实好,刚好数据库操作是 io 操作,可以用异步在 fastapi 中使用sqlalchemy
orm 对象模型映射 (字符串拼接大法好)
为了写异步的 pythonweb 得选择提供异步支持的库 aiofiles 代替正常文件读写,aiomysql+pymysql 数据库引擎对于一个函数如果同步异步效率都一样,或者说没有等待 (io) 操作,那就写同步,更容易理解和调试
sqlalchemy 异步的坑,使用create_async_engine
,async with Session() as session:
,await session.execute(sql)
等而不是常规的创建引擎与会话
sqlalchemy 中使用 relationship 可通过一个表对象获取另一个表中数据,很方便。但在异步代码中报错,Traceback 上百行,大量的 await loop send 等关键字,开始根本不知道是 relationship 的问题,报错只会说事件循环提前退出什么的,单独把代码块拎出来调试,不能直接()
调用也很麻烦,最后发现是 relationship 的问题,干脆不用了,直接手动在多张表中 crud,确实麻烦,还容易出错。
网上 sqlalchemy 的使用方法有同步的有异步的,但是你并不知道哪个函数是异步的,比如同步中使用的是query
,select
,异步中没有query
函数,但是有select
,名字是一样,但是是 sqlalchemy 的不同路径导入的,为什么不看官网(只能说 sqlalchemy 文档太乱了,版本之间函数名字,一言难尽)
由于 sqlalchemy2.0 发布时间不长
Release: 2.0.9 | Release Date: April 5, 2023
sqlalchemy 是支持异步的(从 create_async_engine 函数名看)但文档不全,还不完善,函数名和同步的一模一样
总结异步的缺点:
- 复杂性高,异步编程需要使用回调函数、协程、事件循环等一系列概念和技术,学习和使用成本高。
- 异步更容易出错,调试困难。由于异步编程的执行流程比较复杂,调试错误比同步编程困难。
异步难点:控制不住自己写的代码,因为执行顺序不可预料。它压榨 cpu 时间,让 cpu 不能闲着
时间就像海绵里水一样,只要你愿挤,总还是有。—— 鲁迅
参考:
https://fastapi.tiangolo.com/zh/async/#is-concurrency-better-than-parallelism
https://docs.python.org/zh-cn/3/library/asyncio-task.html#coroutines