# Python 異步與同步操作整合教學 本文件介紹如何在 Python 異步程式設計中處理同步與非阻塞操作,並在 FastAPI 中實作範例。重點包含: * 同步函式與異步函式的區別 * 何謂非阻塞操作 * 如何判斷函數是否可等待 * 如何將同步函式轉換為非阻塞操作(改寫或包裝) * FastAPI 中的應用範例 ## 同步 vs. 異步函式 - 同步函式 直接執行會阻塞後續程式碼,例如: ```python= import time def blocking_task(): print("開始阻塞等待") time.sleep(3) # 阻塞操作 print("等待結束") ``` - 異步函式 使用 async def 定義,並用 await 進行非阻塞等待: ```python= import asyncio async def async_task(): print("開始等待") await asyncio.sleep(3) # 非阻塞等待 print("等待結束") ``` ## 何謂非阻塞操作 非阻塞操作:在等待 I/O 或耗時操作時,事件迴圈仍能處理其他任務,避免整個程式「卡住」。 例子: * 使用 asyncio.sleep():在等待時不阻塞其他任務。 * 使用 aiohttp:進行網路請求時不阻塞主事件迴圈。 ## 如何判斷一個函數是否可等待 利用 inspect 模組檢查函式或物件: - 判斷是否為協程函式 ```python= import inspect async def async_func(): return 123 def sync_func(): return 456 print(inspect.iscoroutinefunction(async_func)) # True print(inspect.iscoroutinefunction(sync_func)) # False ``` - 判斷返回物件是否可等待 ```python= import inspect coro = async_func() print(inspect.isawaitable(coro)) # True ``` - 自定義可等待物件:實現 __await__() 方法的物件也是可等待的。 ## 將同步函式改成異步 有兩種常見做法 - 改寫為異步函式 - 使用包裝(asyncio.to_thread) ### 改寫為異步函式 如果函式內部有非阻塞版本操作,可以直接改寫: ```python= import asyncio async def async_task(): print("開始等待") await asyncio.sleep(3) print("等待結束") ``` ### 使用包裝(asyncio.to_thread) ```python= import asyncio import time def blocking_task(): print("開始阻塞等待") time.sleep(3) print("等待結束") async def async_wrapper(): await asyncio.to_thread(blocking_task) # 執行異步包裝的函式 asyncio.run(async_wrapper()) ``` ## FastAPI 中的應用範例 - 使用 asyncio.to_thread 處理同步操作 在 FastAPI 中,由於底層操作(例如與 MinIO 的連線)為同步,需使用 asyncio.to_thread 來避免阻塞事件迴圈。 假設你有一個同步的 MinioClient 類別,其部分方法如 insert、read 等都是同步實作,範例如下: ```python= from fastapi import FastAPI, Depends import asyncio import time app = FastAPI() # 假設這是你的同步 MinioClient 類別(部分示例) class MinioClient: def insert(self, data: dict) -> dict: # 模擬同步上傳操作 time.sleep(3) return {"status": "uploaded", "data": data} def read(self, identifier: str) -> str: time.sleep(1) return f"data for {identifier}" def close(self) -> None: print("MinioClient connection pool closed") # 全局單例:在應用啟動時初始化,應用關閉時釋放資源 minio_client = None @app.on_event("startup") async def startup_event(): global minio_client minio_client = MinioClient() @app.on_event("shutdown") async def shutdown_event(): if minio_client: minio_client.close() def get_minio_client(): return minio_client # 非阻塞上傳 API,使用 asyncio.to_thread 包裝同步方法 @app.post("/upload") async def upload_object(data: dict, client: MinioClient = Depends(get_minio_client)): result = await asyncio.to_thread(client.insert, data) return result # 非阻塞讀取 API @app.get("/read/{identifier}") async def read_object(identifier: str, client: MinioClient = Depends(get_minio_client)): result = await asyncio.to_thread(client.read, identifier) return {"result": result} ``` ## 連線池的生命週期管理 不希望每次 API 請求後就關閉連線池,可以利用 FastAPI 的啟動與關閉事件來控制連線池的建立與釋放,確保連線池只在應用啟動時初始化,在應用終止時才關閉。 較舊版本 ```python= @app.on_event("startup") async def startup_event(): global minio_client minio_client = MinioClient() # 單例,整個應用生命週期共用 @app.on_event("shutdown") async def shutdown_event(): if minio_client: minio_client.close() # 只有在應用關閉時釋放連線池 ``` 新版本 ```python= async def lifespan(app: FastAPI): # 启动前的操作 global minio_client minio_client = MinioClient() await startup_instance(app) yield # 关闭前的操作 minio_client.close() app = FastAPI(lifespan=lifespan) ``` # 結論 - 非阻塞操作 能讓你的應用在等待 I/O 時依然處理其他任務,提高效能。 - 若底層函式是同步的,可以使用 asyncio.to_thread 在 FastAPI 中包裝,使其非阻塞。 - 透過 FastAPI 的啟動與關閉事件,可以管理全局單例資源(如連線池)的生命週期,避免每次請求都關閉連線。 這些技巧可以讓你在設計高效的異步 API 時,更靈活地整合同步和異步操作。
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up