學習 Python 網頁框架 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 對象模型映射(字符串拼接大法好)。
為了寫異步的 Python 網頁得選擇提供異步支持的庫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 文檔太亂了,版本之間函數名字,一言難盡)。
由於 SQLAlchemy 2.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