在某些場景下(如管理網絡資源的鏈接創建、斷開),用支持異步的上下文管理器是很方便的。web
那麼如何理解async with
關鍵字?編程
先理解普通上下文管理器是靠魔法方法來提供功能的,若是將這個方法用coroutine函數代替呢,那就是async with
的工做原理,看下僞代碼:服務器
class Connection: def __init__(self): self.host = host self.port = port async def __aenter__(self): # 1 self.conn = await get_conn(self.host, self.port) return conn async def __aexit__(self, exc_type, tb): # 2 await self.conn.close() async with Connection('localhost', 8081) as conn: <do something with conn>
__enter__用於同步上下文,__aenter__用於異步上下文;網絡
一樣的,用__aexit__替代__exit__,參數也相同,若是要在代碼中拋出異常就填充參數。併發
僅在使用異步IO時使用異步上下文,若是代碼中沒有阻塞IO,就用普通的上下文管理器。異步
其實這種經過__enter__和__exit__定義上下文管理器的方式有些過期了,咱們經過標準庫contextlib
中的@contextmanager
裝飾器將一個函數包裝成上下文管理器,能夠想到,相似的確定還有@asynccontextmanager
,不過是從3.7纔有的。socket
先看看在同步代碼中如何使用@contextmanager
。async
from contextlib import contextmanager @contextmanager # 1 def web_page(url): data = download_webpage(url) # 2 yield data update_stats(url) # 3 with web_page('google.com') as data: # 4 process(data) # 5
這個裝飾器將生成器函數轉變爲上下文管理器;函數
這個函數調用相似網絡接口調用,速度比CPU慢幾個數量級,一般這個上下文管理器必須在單獨的線程中運行,不然整個程序都會阻塞在這裏;oop
這個函數調用一般是用來統計數據的,從併發的角度來看,須要知道這個函數是否涉及到IO調用,若是有則該函數也應該是阻塞的;
在這裏調用上下文管理器,注意網絡調用隱藏在上下文管理器的內部構造中;
這個函數多是非阻塞(CPU處理少許計算)、半阻塞(固態硬盤等比網絡IO更快)、阻塞(網絡IO)、噩夢阻塞(大量CPU計算)的,這裏假設它是非阻塞的。
如今看看異步下的例子。
from contextlib import asynccontextmanager @asynccontextmanager # 1 async def web_page(url): # 2 data = await download_webpage(url) # 3 yield data # 4 await update_stats(url) # 5 async with web_page('google.com') as data: # 6 process(data)
新的異步裝飾器;
須要這個被裝飾的生成器函數用async def聲明;
在前一個例子中可能會阻塞在這裏,如今用await來促使loop能夠在阻塞時切換到其它工做中,但要注意對於這個download_webpage函數自己,也要將其轉換爲與await關鍵字兼容的coroutine,若是沒法轉換,處理的方法在後面介紹;
如前一個例子,數據被提供給上下文管理器主體,一般應該在內部使用try/finally來捕獲異常,同時注意,yield將函數變成生成器函數,async def將函數變成協程函數,同時調用,返回的是異步生成器函數,調用生成異步生成器,能夠經過inspect庫的isasyncgenfunction()
和isasyncgen()
來判斷類型;
這裏假設咱們將這個函數轉換爲coroutine,所以能夠經過await調用;
上下文管理器的調用也變成異步的了。
在上述例子中,提到了download_webpage()
和update_stats()
函數可能不是那麼容易修改爲async def聲明的coroutine,由於異步支持須要在socket層面進行修改。
大多數場景下,代碼函數都是阻塞的,也幾乎不可能將這些函數修改成非阻塞的。尤爲是在使用第三方庫時,如requests
庫就徹底地使用同步的調用。
爲了解決這個問題,咱們能夠經過executor調用,來實如今異步代碼中調用同步程序。
from contextlib import asynccontextmanager @asynccontextmanager async def web_page(url): # 1 loop = asyncio.get_event_loop() data = await loop.run_in_executor(None, download_webpage, url) # 2 yield data await loop.run_in_executor(None, update_stats, url) # 3 async with web_page('google.com') as data: process(data)
在這個例子中,假設download_webpage和update_stats函數沒法轉換爲coroutine,基於事件循環的編程中最大的錯誤就是阻塞了loop執行,爲了解決這個問題,咱們經過executor在單獨的線程中運行這些同步調用,executor是做爲loop自己的屬性來使用的;
這裏咱們調用executor,其調用原型是AbstractEventLoop.run_in_executor(executor, func, *args)
,對executor參數傳遞None將使用默認的線程池;
在獨立線程中運行另外一個阻塞調用,必須在以前使用await,由於咱們的上下文管理器是一個異步生成器,要在執行以前等待調用完成。
異步上下文管理器在asyncio編程中大量使用,因此仍是有必要好好了解它們,能夠經過官方文檔進一步瞭解。
目前asyncio庫在Python開發團隊中仍處於活躍開發狀態,並在3.7中又增長了一些重要的改進。
asyncio.run()
,用於做爲asyncio程序的主入口;asyncio.create_task()
,不須要loop便可建立task實例;AbstractEventLoop.sock_sendfile()
,使用高性能的os.sendfile()
接口來在TCP socket上發送文件;AbstractEventLoop.start_tls()
,將現有鏈接升級爲TLS;asyncio.Server.serve_forever()
,一個建立asyncio網絡服務器的簡潔接口。