学习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