深刻Asyncio(七)異步上下文管理器

Async Context Managers: async with

在某些場景下(如管理網絡資源的鏈接創建、斷開),用支持異步的上下文管理器是很方便的。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>
  1. __enter__用於同步上下文,__aenter__用於異步上下文;網絡

  2. 一樣的,用__aexit__替代__exit__,參數也相同,若是要在代碼中拋出異常就填充參數。併發

僅在使用異步IO時使用異步上下文,若是代碼中沒有阻塞IO,就用普通的上下文管理器。異步

其實這種經過__enter__和__exit__定義上下文管理器的方式有些過期了,咱們經過標準庫contextlib中的@contextmanager裝飾器將一個函數包裝成上下文管理器,能夠想到,相似的確定還有@asynccontextmanager,不過是從3.7纔有的。socket


contextlib

先看看在同步代碼中如何使用@contextmanagerasync

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
  1. 這個裝飾器將生成器函數轉變爲上下文管理器;函數

  2. 這個函數調用相似網絡接口調用,速度比CPU慢幾個數量級,一般這個上下文管理器必須在單獨的線程中運行,不然整個程序都會阻塞在這裏;oop

  3. 這個函數調用一般是用來統計數據的,從併發的角度來看,須要知道這個函數是否涉及到IO調用,若是有則該函數也應該是阻塞的;

  4. 在這裏調用上下文管理器,注意網絡調用隱藏在上下文管理器的內部構造中;

  5. 這個函數多是非阻塞(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)
  1. 新的異步裝飾器;

  2. 須要這個被裝飾的生成器函數用async def聲明;

  3. 在前一個例子中可能會阻塞在這裏,如今用await來促使loop能夠在阻塞時切換到其它工做中,但要注意對於這個download_webpage函數自己,也要將其轉換爲與await關鍵字兼容的coroutine,若是沒法轉換,處理的方法在後面介紹;

  4. 如前一個例子,數據被提供給上下文管理器主體,一般應該在內部使用try/finally來捕獲異常,同時注意,yield將函數變成生成器函數,async def將函數變成協程函數,同時調用,返回的是異步生成器函數,調用生成異步生成器,能夠經過inspect庫的isasyncgenfunction()isasyncgen()來判斷類型;

  5. 這裏假設咱們將這個函數轉換爲coroutine,所以能夠經過await調用;

  6. 上下文管理器的調用也變成異步的了。


在上述例子中,提到了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)
  1. 在這個例子中,假設download_webpage和update_stats函數沒法轉換爲coroutine,基於事件循環的編程中最大的錯誤就是阻塞了loop執行,爲了解決這個問題,咱們經過executor在單獨的線程中運行這些同步調用,executor是做爲loop自己的屬性來使用的;

  2. 這裏咱們調用executor,其調用原型是AbstractEventLoop.run_in_executor(executor, func, *args),對executor參數傳遞None將使用默認的線程池;

  3. 在獨立線程中運行另外一個阻塞調用,必須在以前使用await,由於咱們的上下文管理器是一個異步生成器,要在執行以前等待調用完成。


異步上下文管理器在asyncio編程中大量使用,因此仍是有必要好好了解它們,能夠經過官方文檔進一步瞭解。

目前asyncio庫在Python開發團隊中仍處於活躍開發狀態,並在3.7中又增長了一些重要的改進。

  1. asyncio.run(),用於做爲asyncio程序的主入口;
  2. asyncio.create_task(),不須要loop便可建立task實例;
  3. AbstractEventLoop.sock_sendfile(),使用高性能的os.sendfile()接口來在TCP socket上發送文件;
  4. AbstractEventLoop.start_tls(),將現有鏈接升級爲TLS;
  5. asyncio.Server.serve_forever(),一個建立asyncio網絡服務器的簡潔接口。
相關文章
相關標籤/搜索