深刻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网络服务器的简洁接口。
相关文章
相关标签/搜索