不看官方文檔,這個問題你可能會束手無策
- 2020 年 3 月 13 日
- 筆記

攝影:產品經理
產品經理親自下廚做的雞 jio jio
在 Python 3.7版本開始,引入了新功能asyncio.run
來快速運行一段非同步程式碼。
例如對於一段使用 aiohttp 請求網址的程式碼,在 Python 3.6或者之前的版本,我們是這樣寫的:
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())
運行效果如下圖所示:

現在有了asyncio.run
,我們可以少敲幾次鍵盤:
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())
運行效果如下圖所示:

這個功能真是太方便了!我準備完全不使用老式寫法了。直到有一天,我使用 Motor 讀取數據。
Motor 是用來非同步讀寫 MongoDB 的庫。我寫程式碼一般會先寫一段 Demo,確認沒有問題了再把 Demo 改成正式程式碼。我們用 Motor寫一段讀取 MongoDB 的程式碼:
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