Python?asyncio的一個(gè)坑
我們先從一個(gè)常見(jiàn)的Python
編程錯(cuò)誤開(kāi)始說(shuō)起,我已經(jīng)見(jiàn)過(guò)非常多的程序員犯過(guò)這種錯(cuò)誤了:
def do_not_raise(user_defined_logic): try: user_defined_logic() except: logger.warning("User defined logic raises an exception", exc_info=True) # ignore
這段代碼的錯(cuò)誤之處在哪里呢?
我們從Python
的異常結(jié)構(gòu)開(kāi)始說(shuō)起。Python
中的異?;?lèi)有兩個(gè),最基礎(chǔ)的是BaseException
,第二個(gè)是Exception
(繼承BaseException
)。這兩者有什么區(qū)別呢?
Exception
代表大部分我們經(jīng)常會(huì)在業(yè)務(wù)邏輯中處理到的異常,也包括一部分運(yùn)行出錯(cuò)例如NameError
、AttributeError
等等。但是并不是所有的異常都是Exception
類(lèi)的子類(lèi),少數(shù)幾個(gè)異常是繼承于BaseException
的:
- GeneratorExit
- SystemExit
- KeyboardInterrupt
第一個(gè)代表生成器被close()
方法關(guān)閉,第二個(gè)代表系統(tǒng)退出(例如使用sys.exit
),第三個(gè)代表程序被Ctrl+C
中斷。之所以它們并不繼承于Exception
,是因?yàn)椋核鼈円话闱闆r下絕不應(yīng)當(dāng)被捕獲,或者被捕獲之后應(yīng)當(dāng)立即reraise
(通過(guò)不帶參數(shù)的raise語(yǔ)句)。
如果寫(xiě)出上面那樣的語(yǔ)句,就可能會(huì)出現(xiàn)程序無(wú)法退出的情況:從外部發(fā)送SIGTERM信號(hào)到程序,觸發(fā)了SystemExit,然而SystemExit被捕獲然后忽略了,這樣程序就沒(méi)有正常退出,而是繼續(xù)執(zhí)行下去。像SystemExit、KeyboardInterrupt、GeneratorExit這樣的異常,因?yàn)闆](méi)有固定的拋出位置,所以如果亂捕獲的話(huà)非常危險(xiǎn),很可能產(chǎn)生隱含的bug,而且測(cè)試中會(huì)很難發(fā)現(xiàn)。這就是為什么Python官方文檔上會(huì)強(qiáng)調(diào),如果使用無(wú)參數(shù)的except
,一定要配合raise重新將異常拋出。而正確的忽略執(zhí)行異常的方法應(yīng)該是:
def do_not_raise(user_defined_logic): try: user_defined_logic() except Exception: ### <= Notice here ### logger.warning("User defined logic raises an exception", exc_info=True) # ignore
那么說(shuō)了這么多,跟asyncio有什么聯(lián)系呢?
在asyncio
當(dāng)中,一個(gè)異步過(guò)程可以通過(guò)asyncio.Task
作為一個(gè)獨(dú)立執(zhí)行的單元啟動(dòng),這個(gè)Task對(duì)象有一個(gè)cancel()方法,可以將它從中途強(qiáng)制停止。類(lèi)似的,異步生成器也可以通過(guò)aclose()
方法強(qiáng)制結(jié)束。當(dāng)一個(gè)異步過(guò)程或者異步生成器被從外部強(qiáng)制中止的時(shí)候,會(huì)從當(dāng)前的await
或者yield
語(yǔ)句拋出asyncio.CancelledError
。
問(wèn)題就出在這個(gè)CancelledError上!
asyncio也許是為了偷懶,也許是為了和concurrent
一致,這個(gè)異常實(shí)際上是concurrent.futures.CancelledError
。它的基類(lèi)是Exception
,而不是BaseException
。要知道,在concurrent
庫(kù)當(dāng)中,CancelledError
是不會(huì)拋到已經(jīng)開(kāi)始了的子過(guò)程中的,它只會(huì)從future對(duì)象里拋出;而asyncio中,當(dāng)使用了cancel()方法的時(shí)候,這個(gè)異常會(huì)從Task的當(dāng)前堆棧位置拋出來(lái)。
這個(gè)事情就尷尬了,如果前面的do_not_raise
是個(gè)異步方法,用 except Exception
來(lái)捕獲了用戶(hù)自定義方法中的異常,那CancelledError
也會(huì)被捕獲到。結(jié)果就是CancelledError
被錯(cuò)誤地忽略掉,導(dǎo)致cancel()方法沒(méi)有成功終止掉一個(gè)Task。
更尷尬的事情在于這個(gè)CancelledError
的拋出機(jī)制。asyncio
內(nèi)部使用了Python的生成器和yield from
機(jī)制,yield from可以自動(dòng)代理異常,
為了說(shuō)明這一點(diǎn)我們考慮下面的代碼:
import traceback import asyncio async def func1(): try: return await func2() except Exception: traceback.print_exc() raise async def func2(): try: await asyncio.sleep(2) except Exception: traceback.print_exc() raise async def func3(): t1 = asyncio.ensure_future(func1()) await asyncio.sleep(1) t1.cancel() try: await t1 except CancelledError: pass
在t1.cancel()
這里,會(huì)發(fā)生什么呢?實(shí)際上異常會(huì)從最內(nèi)層的func2開(kāi)始拋出,從func2拋出到func1,再到func3的await t1,所以可以看到兩次traceback
打印。
這就是異步方法中await的異常代理機(jī)制,它像同步調(diào)用一樣,有完整的堆棧,并且異常從最內(nèi)層拋出。這本身是一個(gè)很好的設(shè)計(jì),很方便調(diào)試,但是一旦CancelledError
拋出,你是無(wú)法確定它具體從哪條語(yǔ)句拋出的,這樣在寫(xiě)異步邏輯的時(shí)候,實(shí)際上必須假設(shè)所有的await語(yǔ)句都有可能拋出CancelledError。如果在外面加上了前面的do_not_raise
這樣的機(jī)制,就會(huì)錯(cuò)誤地忽略掉CancelledError
。
所以異步邏輯中的忽略異常必須寫(xiě)成:
async def do_not_raise(user_defined_coroutine): try: await user_defined_coroutine except CancelledError: raise except Exception: logger.warning("User defined logic raises an exception", exc_info=True) # ignore
這樣才能保證CancelledError
不被錯(cuò)誤捕獲。
從這個(gè)結(jié)果上來(lái)看,CancelledError
從一開(kāi)始就不應(yīng)該繼承自Exception
,它應(yīng)該是一個(gè)BaseException
,這樣就可以減少很多異步編程中的錯(cuò)誤。
并不是自己不調(diào)用cancel()就不會(huì)出現(xiàn)這樣的問(wèn)題。一些會(huì)觸發(fā)cancel()過(guò)程的常見(jiàn)例子包括:
asyncio.wait_for
在執(zhí)行超時(shí)的時(shí)候會(huì)自動(dòng)cancel內(nèi)部的過(guò)程,這是一個(gè)很常用的實(shí)現(xiàn)超時(shí)邏輯的方法
aiohttp的handler
,如果沒(méi)有處理完成之前用戶(hù)就關(guān)閉了HTTP連接(比如強(qiáng)制點(diǎn)了瀏覽器的停止按鈕),會(huì)對(duì)handler的異步過(guò)程調(diào)用cancel()
……
還有更尷尬的事情,許多時(shí)候我們不得不捕獲CancelledError
。剛才的一段代碼,我故意沒(méi)有提,讀者們是否發(fā)現(xiàn)問(wèn)題了呢?
t1.cancel() try: await t1 except CancelledError: pass
在asyncio中,cancel()方法并不會(huì)立即結(jié)束一個(gè)異步Task,它只會(huì)拋出CancelledError
,但是異步過(guò)程有機(jī)會(huì)使用except
或者finally,在退出之前執(zhí)行一些清理過(guò)程。這里的await的本意也是等待t1完全退出再繼續(xù)。但是t1會(huì)拋出CancelledError
,所以捕獲這個(gè)異常,不讓它再拋出。(而且如果不這么做,asyncio
會(huì)打印一行warning
,表示一個(gè)異步Task失敗沒(méi)有被處理)
那么問(wèn)題就來(lái)了:如果func3()在執(zhí)行到這里的時(shí)候,又被外部代碼cancel()
了呢?下面的except CancelledError
就會(huì)變成問(wèn)題,它會(huì)錯(cuò)誤捕獲外部的CancelledError
。另外,t1也會(huì)再次被cancel一遍(沒(méi)錯(cuò),await一個(gè)Task的時(shí)候,如果await所在過(guò)程被cancel,Task也會(huì)被cancel,需要使用asyncio.shield
來(lái)規(guī)避)
正確的寫(xiě)法應(yīng)該是:
t1.cancel() await asyncio.wait([t1]) try: await t1 except CancelledError: pass
asyncio.wait
等待Task執(zhí)行結(jié)束,但并不收集結(jié)果,因此內(nèi)層的CancelledError
不會(huì)在這里拋出來(lái),而且如果此時(shí)取消func3,CancelledError
并不會(huì)被忽略。第二個(gè)await t1時(shí),t1可以保證已經(jīng)結(jié)束,這里內(nèi)部沒(méi)有其他異步等待過(guò)程,因此CancelledError
不會(huì)拋出在這里。也可以用t1.exception()
之類(lèi)代替。
到此這篇關(guān)于Python asyncio的一個(gè)坑的文章就介紹到這了,更多相關(guān)Python asyncio的一個(gè)坑內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
淺析pytorch中對(duì)nn.BatchNorm2d()函數(shù)的理解
Batch Normalization強(qiáng)行將數(shù)據(jù)拉回到均值為0,方差為1的正太分布上,一方面使得數(shù)據(jù)分布一致,另一方面避免梯度消失,這篇文章主要介紹了pytorch中對(duì)nn.BatchNorm2d()函數(shù)的理解,需要的朋友可以參考下2023-11-11Django多數(shù)據(jù)庫(kù)的實(shí)現(xiàn)過(guò)程詳解
這篇文章主要介紹了Django多數(shù)據(jù)庫(kù)的實(shí)現(xiàn)過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08Flask框架響應(yīng)、調(diào)度方法和藍(lán)圖操作實(shí)例分析
這篇文章主要介紹了Flask框架響應(yīng)、調(diào)度方法和藍(lán)圖操作,結(jié)合實(shí)例形式分析了Flask框架中響應(yīng)、調(diào)度方法和藍(lán)圖相關(guān)功能、使用方法及操作注意事項(xiàng),需要的朋友可以參考下2018-07-07caffe的python接口之手寫(xiě)數(shù)字識(shí)別mnist實(shí)例
這篇文章主要為大家介紹了caffe的python接口之手寫(xiě)數(shù)字識(shí)別mnist實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06Python爬蟲(chóng)之爬取嗶哩嗶哩熱門(mén)視頻排行榜
這篇文章主要介紹了Python爬蟲(chóng)之爬取嗶哩嗶哩熱門(mén)視頻排行榜,文中有非常詳細(xì)的代碼示例,對(duì)正在學(xué)習(xí)python的小伙伴們有非常好的幫助,需要的朋友可以參考下2021-04-04Python設(shè)計(jì)模式之門(mén)面模式簡(jiǎn)單示例
這篇文章主要介紹了Python設(shè)計(jì)模式之門(mén)面模式,簡(jiǎn)單描述了門(mén)面模式的概念、原理,并結(jié)合實(shí)例形式給出了Python定義與使用門(mén)面模式的具體操作技巧,需要的朋友可以參考下2018-01-01Python退出時(shí)強(qiáng)制運(yùn)行一段代碼的實(shí)現(xiàn)方法
這篇文章主要介紹了Python退出時(shí)強(qiáng)制運(yùn)行一段代碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04VSCode下配置python調(diào)試運(yùn)行環(huán)境的方法
這篇文章主要介紹了VSCode下配置python調(diào)試運(yùn)行環(huán)境的方法,需要的朋友可以參考下2018-04-04利用python對(duì)月餅數(shù)據(jù)進(jìn)行可視化(看看哪家最劃算)
通過(guò)python對(duì)數(shù)據(jù)進(jìn)行可視化展示,可直觀地展示數(shù)據(jù)之間的關(guān)系,為用戶(hù)提供更多的信息,這篇文章主要給大家介紹了關(guān)于利用python對(duì)月餅數(shù)據(jù)進(jìn)行可視化的相關(guān)資料,看看哪家最劃算,需要的朋友可以參考下2022-09-09