不看官方文檔,這個問題你可能會一籌莫展html
收錄於話題 #你不知道的 Python 71個
攝影:產品經理
產品經理親自下廚作的雞 jio jio
在 Python 3.7版本開始,引入了新功能asyncio.run來快速運行一段異步代碼。python
例如對於一段使用 aiohttp 請求網址的代碼,在 Python 3.6或者以前的版本,咱們是這樣寫的:git
import asyncio import aiohttp async def main(): async with aiohttp.ClientSession() as client: resp = await client.get('http://httpbin.org/ip') ip = await resp.json() print(ip) loop = asyncio.get_event_loop() loop.run_until_complete(main())
運行效果以下圖所示:github
如今有了asyncio.run,咱們能夠少敲幾回鍵盤:mongodb
import asyncio import aiohttp async def main(): async with aiohttp.ClientSession() as client: resp = await client.get('http://httpbin.org/ip') ip = await resp.json() print(ip) asyncio.run(main())
運行效果以下圖所示:json
這個功能真是太方便了!我準備徹底不使用老式寫法了。直到有一天,我使用 Motor 讀取數據。api
Motor 是用來異步讀寫 MongoDB 的庫。我寫代碼通常會先寫一段 Demo,確認沒有問題了再把 Demo 改爲正式代碼。咱們用 Motor寫一段讀取 MongoDB 的代碼:app
import asyncio import motor.motor_asyncio async def main(): client = motor.motor_asyncio.AsyncIOMotorClient() db = client.exercise collection = db.person_info async for doc in collection.find({}, {'_id': 0}): print(doc) asyncio.run(main())
運行效果符合預期,以下圖所示:框架
既然 Demo 能夠正常運行,那麼咱們把這段代碼修改得稍微正式一些,使用類來包住正常的代碼:異步
import asyncio import motor.motor_asyncio class MongoUtil: def __init__(self): conn = motor.motor_asyncio.AsyncIOMotorClient() db = conn.exercise self.collection = db.person_info async def read_people(self): async for doc in self.collection.find({}, {'_id': 0}): print(doc) util = MongoUtil() asyncio.run(util.read_people())
運行效果以下圖所示,居然報錯了:
報錯信息的最後一句,單獨摘錄出來::
RuntimeError: Task <Task pending coro=<MongoUtil.read_people() running at /Users/kingname/test_fastapi/test_motor.py:12> cb=[_run_until_complete_cb() at /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.7/lib/python3.7/asyncio/base_events.py:158]> got Future <Future pending cb=[run_on_executor.._call_check_cancel() at /Users/kingname/.virtualenvs/test_fastapi-v5CW09hz/lib/python3.7/site-packages/motor/frameworks/asyncio/init.py:80]> attached to a different loop」
其中最關鍵的一句話是:attached to a different loop
顯然咱們這個程序是單進程單線程的程序,這段報錯說明當前的這個線程裏面,在運行asyncio.run以前,就已經存在一個事件循環了。而根據 asyncio 的規定,一個線程裏面只能有一個事件循環正在運行,因此就致使報錯。
爲了解釋這個報錯的緣由,咱們來看看 Python 的官方文檔中,asyncio.run的相關說明[1],以下圖所示:
其中畫紅色方框的兩個地方:
This function cannot be called when another asyncio event loop is running in the same thread. 當另外一個 asyncio 事件循環正在當前線程運行的時候,不能調用這個函數。」
This function always creates a new event loop and closes it at the end. 這個函數老是建立一個新的事件循環並在最後(運行完成)關閉它。」
因此,當咱們調用asyncio.run的時候,必須確保當前線程沒有事件循環正在運行。
可是,當咱們在運行上圖第16行代碼,初始化MongoUtil的時候,它的構造函數init會運行,因而第7行代碼就會運行。
來看一下Motor 的官方文檔中關於AsyncIOMotorClient的描述[2]
AsyncIOMotorClient有一個參數叫作io_loop,若是不傳入事件循環對象的話,就會使用默認的。但程序運行到這個位置的時候,尚未誰建立了事件循環,因而Motor就會本身建立一個事件循環。
關於這一點,你們能夠閱讀Motor 的源代碼[3]第150-154行:
在不傳入io_loop的時候,會調用self._framework.get_event_loop()。其中,self._framework多是trio也多是asyncio。由於 Motor 支持這兩種異步框架。咱們這裏使用的是asyncio。因爲當前沒有正在運行的事件循環,因此asyncio.get_event_loop就會建立一個,並讓它運行起來。
因此當咱們使用 Motor 初始化 MongoDB 的鏈接時,就已經建立了一個事件循環了。但當代碼運行到asyncio.run的時候,又準備建立一個新的事件循環,天然而然程序就運行錯了。
因此,要讓程序正常運行,咱們在最後一行不能建立新的事件循環,而是須要獲取由 Motor 已經建立好的事件循環。因此代碼須要改爲老式寫法:
loop = asyncio.get_event_loop() loop.run_until_complete(util.read_people())
這樣一來,程序就能正常工做了:
這個問題經過官方文檔就能找到緣由並解決。但若是你不看官方文檔,而是一味在網上亂搜索,恐怕很難找到解決辦法。
參考資料
[1]
相關說明: https://docs.python.org/3/library/asyncio-task.html#asyncio.run
[2]
描述: https://motor.readthedocs.io/en/stable/api-asyncio/asyncio_motor_client.html#motor.motor_asyncio.AsyncIOMotorClient
[3]
源代碼: https://github.com/mongodb/motor/blob/4c7534c6200e4f160268ea6c0e8a9038dcc69e0f/motor/core.py#L154
kingname攢錢給產品經理買房。