个性化阅读
专注于IT技术分析

Python开发:Asyncio简介

本文概述

asyncio模块在3.4版中作为临时包添加到了Python中。这意味着asyncio可能会收到向后不兼容的更改, 甚至可能在将来的Python版本中删除。

根据该文档, asyncio”提供了使用协程编写单线程并发代码, 通过套接字和其他资源进行I / O复用访问, 运行网络客户端和服务器以及其他相关原语的基础结构”。本章并不打算涵盖你可以使用asyncio进行的所有操作, 但是你将学习如何使用该模块以及它为何有用。

如果你在旧版本的Python中需要类似asyncio的东西, 那么你可能想看看Twisted或gevent。

定义

asyncio模块提供了一个围绕事件循环的框架。事件循环基本上等待事件发生, 然后对事件进行操作。它负责处理I / O和系统事件。

asyncio实际上有几个可用的循环实现。默认情况下, 该模块将是运行该模块的操作系统中效率最高的模块。但是, 你可以根据需要明确选择事件循环。事件循环基本上说”事件A发生时, 对功能B做出反应”。

当服务器在等待某人来请求资源(例如网页)时, 请考虑一下服务器。如果该网站不是很受欢迎, 则服务器将长时间闲置。但是, 当确实受到打击时, 服务器需要做出反应。这种反应称为事件处理。当用户加载网页时, 服务器将检查并调用一个或多个事件处理程序。这些事件处理程序完成后, 它们需要将控制权交还给事件循环。为此, asyncio使用协程。

协程是一种特殊功能, 可以在不丢失其状态的情况下放弃对其调用方的控制。协程是消费者, 是生成器的扩展。与线程相比, 它们的一大优点是它们不需要占用太多内存来执行。请注意, 调用协程函数时, 该函数实际上不会执行。相反, 它将返回一个协程对象, 你可以将其传递到事件循环以使其立即执行或稍后执行。

使用asyncio模块时, 你可能会碰到另一个术语。基本上, 未来是代表尚未完成的工作结果的对象。你的事件循环可以监视将来的对象并等待它们完成。将来完成时, 它将设置为完成。 Asyncio还支持锁和信号灯。

我要提及的最后一条信息是”任务”。任务是协程的包装, 也是Future的子类。你甚至可以使用事件循环安排任务。

异步等待

在Python 3.5中添加了async和await关键字, 以定义本机协程, 并使其与基于生成器的协程相比具有不同的类型。如果你想深入了解异步和等待状态, 请查看PEP 492。

在Python 3.4中, 你将创建一个这样的协程:

该装饰器仍可在Python 3.5中使用, 但类型模块收到了协程函数形式的更新, 该更新现在将告诉你与之交互的是本机协程。从Python 3.5开始, 你可以使用async def在语法上定义协程函数。

因此, 上面的函数最终看起来像这样:

以这种方式定义协程时, 不能在协程函数内部使用yield。相反, 它必须包含用于将值返回给调用方的return或await语句。请注意, 关键字await只能在异步def函数中使用。

可以将async / await关键字视为用于异步编程的API。 asyncio模块只是碰巧使用async / await进行异步编程的框架。实际上, 有一个名为curio的项目可以证明这一概念, 因为它是事件循环的单独实现, 该事件循环在幕后使用了async / wait。

一个不好的协程示例

尽管有很多背景信息对所有这些工作原理无疑是有帮助的, 但有时你只想看一些示例, 以便对语法以及如何将它们组合在一起有个了解。

因此, 请记住一个简单的例子!

你将要完成的一个相当普通的任务是从某个位置下载文件, 无论是内部资源还是Internet上的文件。通常, 你将要下载多个文件。

因此, 让我们创建一对可以做到这一点的协程:


import asyncio
import os
import urllib.request
 
async def download_coroutine(url):
    #"A coroutine to download the specified url"
    request = urllib.request.urlopen(url)
    filename = os.path.basename(url)
 
    with open(filename, 'wb') as file_handle:
        while True:
            chunk = request.read(1024)
            if not chunk:
                break
            file_handle.write(chunk)
    msg = 'Finished downloading {filename}'.format(filename=filename)
    return msg
 
async def main(urls):
    """
    Creates a group of coroutines and waits for them to finish
    """
    coroutines = [download_coroutine(url) for url in urls]
    completed, pending = await asyncio.wait(coroutines)
    for item in completed:
        print(item.result())
 
 
if __name__ == '__main__':
    urls = ["http://www.irs.gov/pub/irs-pdf/f1040.pdf", "http://www.irs.gov/pub/irs-pdf/f1040a.pdf", "http://www.irs.gov/pub/irs-pdf/f1040ez.pdf", "http://www.irs.gov/pub/irs-pdf/f1040es.pdf", "http://www.irs.gov/pub/irs-pdf/f1040sb.pdf"]
 
    event_loop = asyncio.get_event_loop()
    try:
        event_loop.run_until_complete(main(urls))
    finally:
        event_loop.close()

在这段代码中, 我们导入所需的模块, 然后使用异步语法创建第一个协程。这个协程称为download_coroutine, 它使用Python的urllib下载传递给它的所有URL。完成后, 它将返回一条消息, 内容是这样的。

另一个协程是我们的主要协程。它基本上获取一个或多个URL的列表并将它们排队。我们使用asyncio的wait函数来等待协程完成。当然, 要实际启动协程, 需要将它们添加到事件循环中。我们在获得事件循环的最后执行此操作, 然后调用其run_until_complete方法。你会注意到, 我们将主协程传递给了事件循环。这开始运行主协程, 该协程将第二个协程排入队列并开始运行。这被称为链式协程。

这个例子的问题在于它根本不是协程。原因是download_coroutine函数不是异步的。这里的问题是urllib不是异步的, 而且, 我也没有使用await或yield from这两者。更好的方法是使用aiohttp包。让我们接下来看看!

一个更好的协程示例

aiohttp软件包用于创建异步HTTP客户端和服务器。你可以使用pip安装它, 如下所示:


pip install aiohttp

安装完成后, 让我们更新代码以使用aiohttp, 以便我们下载文件:

你会在这里注意到我们导入了两个新项目:aiohttp和async_timeout。后者实际上是aiohttp的依赖项之一, 它使我们可以创建超时上下文管理器。让我们从代码的底部开始, 然后逐步进行。在最下面的条件语句中, 我们开始异步事件循环并调用我们的main函数。

在主函数中, 我们创建一个ClientSession对象, 然后将其传递给要下载的每个URL的下载协程函数。在download_coroutine中, 我们创建一个async_timeout.timeout()上下文管理器, 该管理器基本上创建一个X秒的计时器。当秒数用完时, 上下文管理器结束或超时。在这种情况下, 超时为10秒。接下来, 我们调用会话的get()方法, 该方法为我们提供了一个响应对象。现在, 我们进入了一个神奇的部分。当你使用响应对象的content属性时, 它会返回aiohttp.StreamReader的实例, 该实例使我们能够以任意大小下载文件。读取文件时, 会将其写到本地磁盘。最后, 我们调用响应的release()方法, 该方法将完成响应处理。

根据aiohttp的文档, 由于响应对象是在上下文管理器中创建的, 因此从技术上讲, 它隐式调用release()。但是在Python中, 显式通常会更好, 并且在文档中有一条注释, 我们不应该仅仅依赖于连接, 所以我认为在这种情况下最好发布它。

有一部分仍在这里阻塞, 这是实际写入磁盘的代码部分。在写入文件时, 我们仍在阻止。还有另一个名为aiofiles的库, 我们也可以使用该库来尝试使文件写入异步, 但是我将把更新内容留给读者。

安排电话

你还可以使用asyncio事件循环安排对常规函数的调用。我们要看的第一种方法是call_soon。 call_soon方法基本上会尽快调用你的回调或事件处理程序。它用作FIFO队列, 因此, 如果某些回调需要一段时间才能运行, 那么其他回调将被延迟, 直到之前的回调完成为止。

让我们看一个例子:

asyncio的大多数功能都不接受关键字, 因此, 如果需要将关键字传递给事件处理程序, 则需要functools模块。每当调用标准函数时, 它将在stdout中输出一些文本。如果碰巧将其stop参数设置为True, 它也会停止事件循环。

第一次调用它时, 我们不会停止循环。第二次调用它时, 我们确实停止了循环。我们要停止循环的原因是, 我们已将其告知run_forever, 这会将事件循环置于无限循环中。一旦循环停止, 我们就可以关闭它。

如果运行此代码, 则应看到以下输出:


starting event loop
Event handler called
Event handler called
stopping the loop
closing event loop

有一个名为call_soon_threadsafe的相关函数。顾名思义, 它的工作方式与call_soon相同, 但是它是线程安全的。如果你想将呼叫实际延迟到以后的某个时间, 可以使用call_later函数来进行。在这种情况下, 我们可以将我们的call_soon签名更改为以下内容:


loop.call_later(1, event_handler, loop)

这将延迟调用事件处理程序一秒钟, 然后将其调用并将循环作为第一个参数传入。如果你想安排将来的某个特定时间, 则需要获取循环时间而不是计算机时间。你可以这样做:


current_time = loop.time()

一旦有了它, 就可以使用call_at函数并将其传递给它调用事件处理程序的时间。假设我们要从现在开始五分钟打电话给事件处理程序。你可以按照以下方式操作:


loop.call_at(current_time + 300, event_handler, loop)

在此示例中, 我们使用当前获取的时间, 并将其添加300秒或5分钟。这样, 我们将调用事件处理程序的时间延迟了五分钟!漂亮整齐!

任务

任务是Future的子类, 是协程的包装。他们使你能够跟踪完成处理的时间。因为它们是未来的一种, 所以其他协程可以等待任务, 并且在完成任务后也可以获取任务的结果。

让我们看一个简单的例子:

在这里, 我们创建一个异步函数, 该函数接受该函数运行所需的秒数。这模拟了一个长时间运行的过程。然后, 我们创建事件循环, 然后通过调用事件循环对象的create_task函数来创建任务对象。 create_task函数接受我们要转换为任务的函数。然后, 我们告诉事件循环运行, 直到任务完成。最后, 由于任务完成, 我们得到了任务的结果。

通过使用取消方法, 任务也可以非常轻松地取消。要结束任务时只需调用它即可。如果任务在等待其他操作时被取消, 则该任务将引发CancelledError。

包起来

在这一点上, 你应该了解足够的知识, 可以自己开始使用asyncio库。 asyncio库非常强大, 可让你执行许多非常酷和有趣的任务。 Theasyncio库是为网络套接字而设计的。

一个很棒的库, 允许在Twisted框架异步之前进行异步套接字编程。另一个有趣的项目是Dask, 它是一个用于分析计算的灵活并行计算库。只需深入研究asyncio模块的文档, 让你的想法开始流行!

赞(0) 打赏
未经允许不得转载:srcmini » Python开发:Asyncio简介
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!

 

觉得文章有用就打赏一下文章作者

微信扫一扫打赏