xChar
·a year ago

学习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个任务没有加awaitmain() 函数会在创建任务 task1task2 后立即返回,不会等待这两个任务完成,也就是只有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函数名看)但文档不全,还不完善,函数名和同步的一模一样

总结异步的缺点:

  1. 复杂性高,异步编程需要使用回调函数、协程、事件循环等一系列概念和技术,学习和使用成本高。
  2. 异步更容易出错,调试困难。由于异步编程的执行流程比较复杂,调试错误比同步编程困难。

异步难点: 控制不住自己写的代码,因为执行顺序不可预料。它压榨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

Loading comments...