PEP 3156 – 异步 IO 支持重启:“asyncio”模块
- 作者:
- Guido van Rossum <guido at python.org>
- BDFL 委托:
- Antoine Pitrou <antoine at python.org>
- 讨论至:
- python-tulip@googlegroups.com
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2012年12月12日
- Python 版本:
- 3.3
- 发布历史:
- 2012年12月21日
- 取代:
- 3153
- 决议:
- Python-Dev 消息
摘要
这是一份关于Python 3(从Python 3.3开始)中异步I/O的提案。请将此视为PEP 3153中缺失的具体提案。该提案包括一个可插拔的事件循环、类似于Twisted中的传输和协议抽象,以及一个基于yield from(PEP 380)的高级调度器。建议的包名为asyncio。
引言
状态
存在一个代号为Tulip的参考实现。Tulip仓库的链接在末尾的参考文献部分。基于此仓库的包将在PyPI上提供(参见参考文献),以便在Python 3.3安装中使用asyncio包。
截至2013年10月20日,asyncio包已提交到Python 3.4仓库,并随Python 3.4-alpha-4发布,API状态为“ provisional”(临时)。这表达了信心并旨在增加对API的早期反馈,而不是强制接受PEP。预计该包在Python 3.4中将保持临时状态,并在Python 3.5中进展到最终状态。开发仍主要在Tulip仓库中进行,偶尔会将更改合并到CPython仓库。
依赖
许多提议的功能都需要Python 3.3。参考实现(Tulip)除了Python 3.3之外,不需要新的语言或标准库功能,不需要第三方模块或包,也不需要C代码,除了Windows上(可选的)IOCP支持。
模块命名空间
这里的规范存在于一个新的顶级包asyncio中。不同的组件存在于包的独立子模块中。包将从各自的子模块导入通用API,并使其作为包属性可用(类似于email包的工作方式)。对于此类通用API,实际定义它们的子模块名称不属于规范的一部分。不太常见的API可能需要从其各自的子模块中显式导入,在这种情况下,子模块名称是规范的一部分。
没有子模块名称定义的类和函数被认为存在于顶级包的命名空间中。(但不要将这些与各种类的方法混淆,为了简洁起见,在某些上下文中也无需命名空间前缀使用它们。)
互操作性
事件循环是大多数互操作性发生的地方。对于Twisted、Tornado甚至gevents等框架(Python 3.3端口)来说,应该很容易使用轻量级适配器或代理来使其默认事件循环实现适应其需求,或者用其自己的事件循环实现的适配器替换默认事件循环实现。(某些框架,如Twisted,有多个事件循环实现。这应该不是问题,因为它们都具有相同的接口。)
在大多数情况下,两个不同的第三方框架应该能够互操作,通过共享默认事件循环实现(每个框架使用自己的适配器),或者通过共享任一框架的事件循环实现。在后一种情况下,将发生两个级别的适配(从框架A的事件循环到标准事件循环接口,再从那里到框架B的事件循环)。使用哪个事件循环实现应由主程序控制(尽管提供了事件循环选择的默认策略)。
为了使这种互操作性有效,第三方框架中首选的适配方向是保留默认事件循环并将其适配到框架的API。理想情况下,所有第三方框架都将放弃自己的事件循环实现,转而使用标准实现。但并非所有框架都可能对标准实现提供的功能感到满意。
为了支持两个方向的适配,指定了两个独立的API
- 用于管理当前事件循环的接口
- 符合事件循环的接口
事件循环实现可以提供额外的方法和保证,只要这些在文档中被标记为非标准。事件循环实现也可以在给定环境中无法实现某些方法时将其留空;然而,此类偏离标准API的行为应仅作为最后手段考虑,并且仅当平台或环境强制要求时。(一个例子是无法启动或停止系统事件循环的平台;参见下面的“嵌入式事件循环”。)
事件循环API不依赖于await/yield from。相反,它使用回调、附加接口(传输和协议)和Futures的组合。后者类似于PEP 3148中定义的那些,但具有不同的实现并且不与线程绑定。特别是,当结果尚未准备好时,result()方法会引发异常而不是阻塞;用户应使用回调(或await/yield from)来等待结果。
所有指定返回协程的事件循环方法都允许返回Future或协程,由实现选择(标准实现总是返回协程)。所有文档中接受协程参数的事件循环方法*必须*接受Future和协程作为此类参数。(存在一个便利函数ensure_future(),用于将协程或Future的参数转换为Future。)
对于像我这样不喜欢使用回调的用户,提供了一个调度器,用于使用PEP 380 yield from或PEP 492 await表达式将异步I/O代码编写为协程。调度器不可插拔;可插拔性发生在事件循环级别,标准调度器实现应适用于任何符合规范的事件循环实现。(事实上,这是符合规范实现的 litmus 测试。)
为了在协程代码和其他异步框架之间实现互操作性,调度器定义了一个行为类似于Future的Task类。在事件循环级别互操作的框架可以通过向Future添加回调来等待Future完成。同样,调度器提供了一个操作,用于暂停协程直到回调被调用。
如果此类框架不能按原样使用Future和Task类,它可以重新实现loop.create_future()和loop.create_task()方法。这些方法应该返回实现Future/Task接口(或其超集)的对象。
一个不太雄心勃勃的框架可能只会调用loop.set_task_factory()来替换Task类,而不实现自己的事件循环。
事件循环API提供了与线程的有限互操作性:有一个API可以将函数提交给执行器(参见PEP 3148),该API返回一个与事件循环兼容的Future,并且有一个方法可以以线程安全的方式从另一个线程调度事件循环的回调。
传输和协议
对于不熟悉Twisted的人,快速解释一下传输和协议之间的关系是必要的。在最高级别,传输关注字节是如何传输的,而协议决定要传输哪些字节(以及在某种程度上何时传输)。
换句话说:传输是套接字(或类似的I/O端点)的抽象,而协议是从传输的角度看应用程序的抽象。
另一种观点是,传输和协议接口*共同*定义了用于网络I/O和进程间I/O的抽象接口。
传输对象和协议对象之间几乎总是1:1的关系:协议调用传输方法发送数据,而传输调用协议方法将接收到的数据传递给协议。传输和协议方法都不会“阻塞”——它们启动事件然后返回。
最常见的传输类型是双向流传输。它表示一对缓冲流(每个方向一个),每个流传输一个字节序列。双向流传输最常见的例子可能是TCP连接。另一个常见例子是SSL/TLS连接。但也有一些其他事物可以这样看待,例如SSH会话或一对UNIX管道。通常没有许多不同的传输实现,其中大多数都与事件循环实现一起提供。然而,并没有要求所有传输都必须通过调用事件循环方法来创建:第三方模块可以实现新的传输,并为其提供一个构造函数或工厂函数,该函数只需将事件循环作为参数,或者调用get_event_loop()。
请注意,传输不需要使用套接字,即使它们使用TCP也不需要——套接字是平台特定的实现细节。
双向流传输有两个“端”:一端与网络通信(或另一个进程,或它封装的任何低级接口),另一端与协议通信。前者使用实现传输所需的任何API;但传输和协议之间的接口由本PEP标准化。
协议可以表示某种“应用层”协议,例如HTTP或SMTP;它也可以实现多个协议共享的抽象,或者一个完整的应用程序。协议的主要接口是与传输的接口。虽然一些流行的协议(和其他抽象)可能有标准实现,但应用程序通常实现自定义协议。建立有用的第三方协议实现库并从PyPI下载和安装也很有意义。
传输和协议的通用概念包括其他接口,其中传输封装了一些其他通信抽象。示例包括用于发送和接收数据报(例如UDP)的接口,或子进程管理器。关注点的分离与双向流传输和协议相同,但传输和协议之间的具体接口在每种情况下都不同。
各种标准传输和协议类型定义的接口的详细信息将在后面给出。
事件循环接口规范
事件循环策略:获取和设置当前事件循环
事件循环管理由事件循环策略控制,它是一个全局的(每个进程)对象。有一个默认策略,以及一个更改策略的API。策略定义了上下文的概念;策略为每个上下文管理一个独立的事件循环。默认策略的上下文概念被定义为当前线程。
某些平台或编程框架可能会将默认策略更改为更适合该平台或框架用户期望的策略。此类平台或框架必须记录其策略以及在初始化序列的哪个点设置策略,以避免当多个活动框架想要覆盖默认策略时出现未定义行为。(另请参见下面的“嵌入式事件循环”。)
要获取当前上下文的事件循环,请使用get_event_loop()。这将返回一个实现下面指定接口的事件循环对象,或者在未为当前上下文设置事件循环且当前策略未指定创建事件循环的情况下引发异常。它不应返回None。
要为当前上下文设置事件循环,请使用set_event_loop(event_loop),其中event_loop是一个事件循环对象,即AbstractEventLoop的实例,或None。将当前事件循环设置为None是可以的,在这种情况下,后续调用get_event_loop()将引发异常。这对于测试不应依赖默认事件循环存在的代码很有用。
预期get_event_loop()会根据上下文返回不同的事件循环对象(事实上,这就是上下文的定义)。如果未设置且策略允许创建,它可能会创建一个新的事件循环对象。默认策略仅在主线程中创建新的事件循环(由threading.py定义,它为主线程使用一个特殊的子类),并且仅当在调用set_event_loop()之前调用get_event_loop()时。(要重置此状态,请重置策略。)在其他线程中,必须显式设置事件循环。其他策略可能行为不同。默认策略创建的事件循环是惰性的;即,如果必要且当前策略指定,则第一次调用get_event_loop()会创建一个事件循环实例。
为了单元测试和其他特殊情况的需要,还有第三个策略函数:new_event_loop(),它根据策略的默认规则创建并返回一个新的事件循环对象。要使其成为当前事件循环,您必须使用它调用set_event_loop()。
要更改事件循环策略,请调用set_event_loop_policy(policy),其中policy是事件循环策略对象或None。如果不是None,策略对象必须是AbstractEventLoopPolicy的实例,该实例定义了get_event_loop()、set_event_loop(loop)和new_event_loop()方法,所有这些方法的行为都与上述函数相同。
传递None策略值会恢复默认事件循环策略(覆盖平台或框架设置的备用默认策略)。默认事件循环策略是DefaultEventLoopPolicy类的一个实例。可以通过调用get_event_loop_policy()来检索当前事件循环策略对象。
待定:描述子进程处理的子观察者和UNIX怪癖。
显式传递事件循环
可以编写不依赖全局或每线程默认事件循环而使用事件循环的代码。为此,所有需要访问当前事件循环的API(且不是事件类的方法)都接受一个名为loop的可选关键字参数。如果此参数为None或未指定,此类API将调用get_event_loop()以获取默认事件循环,但如果loop关键字参数设置为事件循环对象,它们将使用该事件循环,并将其传递给它们调用的任何其他此类API。例如,Future(loop=my_loop)将创建一个与事件循环my_loop绑定的Future。当默认当前事件为None时,loop关键字参数实际上是强制性的。
请注意,显式传递的事件循环仍必须属于当前线程;loop关键字参数不会神奇地改变事件循环使用方式的限制。
指定时间
与Python中通常一样,所有超时、间隔和延迟都以秒为单位测量,可以是整数或浮点数。然而,绝对时间未指定为POSIX时间戳。时钟的准确性、精度和纪元取决于实现。
默认实现使用time.monotonic()。关于这个选择的含义可以写很多书。最好阅读标准库time模块的文档。
嵌入式事件循环
在某些平台上,事件循环由系统提供。当用户代码启动时,这样的循环可能已经运行,并且可能无法在不退出程序的情况下停止或关闭它。在这种情况下,启动、停止和关闭事件循环的方法可能无法实现,并且is_running()可能总是返回True。
事件循环类
实际上没有名为EventLoop的类。有一个AbstractEventLoop类,它定义了所有没有实现的方法,主要作为文档使用。定义了以下具体类
SelectorEventLoop是基于selectors模块(Python 3.4新增)的完整API的具体实现。构造函数接受一个可选参数,一个selectors.Selector对象。默认情况下,会创建一个selectors.DefaultSelector实例并使用。ProactorEventLoop是API的具体实现,但I/O事件处理和信号处理方法除外。它仅在Windows上(或支持类似“overlapped I/O”API的其他平台上)定义。构造函数接受一个可选参数,一个Proactor对象。默认情况下,会创建一个IocpProactor实例并使用。(IocpProactor类未由本PEP指定;它只是ProactorEventLoop类的一个实现细节。)
事件循环方法概述
符合规范的事件循环的方法分为几个类别。第一组类别必须由所有符合规范的事件循环实现支持,但嵌入式事件循环可能不实现启动、停止和关闭的方法。(然而,部分符合规范的事件循环总比没有好。:-)
- 启动、停止和关闭:
run_forever()、run_until_complete()、stop()、is_running()、close()、is_closed()。 - 基本和定时回调:
call_soon()、call_later()、call_at()、time()。 - 线程交互:
call_soon_threadsafe()、run_in_executor()、set_default_executor()。 - 互联网名称查找:
getaddrinfo()、getnameinfo()。 - 互联网连接:
create_connection()、create_server()、create_datagram_endpoint()。 - 封装的套接字方法:
sock_recv()、sock_sendall()、sock_connect()、sock_accept()。 - 任务和Future支持:
create_future()、create_task()、set_task_factory()、get_task_factory()。 - 错误处理:
get_exception_handler()、set_exception_handler()、default_exception_handler()、call_exception_handler()。 - 调试模式:
get_debug()、set_debug()。
第二组类别*可能*由符合规范的事件循环实现支持。如果不支持,它们将引发NotImplementedError。(在默认实现中,UNIX系统上的SelectorEventLoop支持所有这些;Windows上的SelectorEventLoop支持I/O事件处理类别;Windows上的ProactorEventLoop支持管道和子进程类别。)
- I/O 回调:
add_reader()、remove_reader()、add_writer()、remove_writer()。 - 管道和子进程:
connect_read_pipe()、connect_write_pipe()、subprocess_shell()、subprocess_exec()。 - 信号回调:
add_signal_handler()、remove_signal_handler()。
事件循环方法
启动、停止和关闭
一个(未关闭的)事件循环可以处于两种状态之一:运行中或已停止。这些方法处理事件循环的启动和停止
run_forever()。运行事件循环直到调用stop()。当事件循环已经在运行时不能调用此方法。(它有一个长的名字,部分是为了避免与本PEP的早期版本混淆,其中run()有不同的行为,部分是因为已经有太多API有一个名为run()的方法,部分是因为无论如何不应该在许多地方调用它。)run_until_complete(future)。运行事件循环直到Future完成。如果Future完成,则返回其结果,或引发其异常。当事件循环已经在运行时不能调用此方法。如果参数是协程,则该方法会创建一个新的Task对象。stop()。在方便时立即停止事件循环。之后可以正常使用run_forever()或run_until_complete()重新启动循环;如果这样做,任何已调度回调都不会丢失。注意:stop()正常返回,当前回调可以继续。事件循环在此之后停止的速度取决于实现,但目的是在轮询I/O之前停止,并且不运行任何将来调度的回调;实现的主要自由度是它在停止之前处理“就绪队列”(已使用call_soon()调度的回调)的多少。is_running()。如果事件循环当前正在运行,则返回True,如果已停止,则返回False。close()。关闭事件循环,释放其可能持有的任何资源,例如epoll()或kqueue()使用的文件描述符,以及默认执行器。在事件循环运行时不应调用此方法。调用后,不应再次使用事件循环。可以多次调用;后续调用是空操作。is_closed()。如果事件循环已关闭,则返回True,否则返回False。(主要用于错误报告;请不要基于此方法实现功能。)
基本回调
与同一事件循环关联的回调是严格串行化的:一个回调必须在下一个回调被调用之前完成。这是一个重要的保证:当两个或多个回调使用或修改共享状态时,保证每个回调在运行时,共享状态不会被另一个回调更改。
call_soon(callback, *args)。这会安排一个回调尽快被调用。返回一个表示回调的Handle(见下文),其cancel()方法可用于取消回调。它保证回调按其调度顺序调用。call_later(delay, callback, *args)。安排callback(*args)在大约delay秒后调用一次,除非取消。返回一个表示回调的Handle,其cancel()方法可用于取消回调。过去调度或同时调度的回调将以未定义的顺序调用。call_at(when, callback, *args)。这类似于call_later(),但时间表示为绝对时间。返回一个类似的Handle。有一个简单的等价关系:loop.call_later(delay, callback, *args)与loop.call_at(loop.time() + delay, callback, *args)相同。time()。根据事件循环的时钟返回当前时间。这可能是time.time()或time.monotonic()或其他系统特定时钟,但它必须返回一个浮点数,表示自某个纪元以来大约一秒为单位的时间。(没有完美的时钟——参见PEP 418。)
注意:本PEP的早期版本定义了一个名为call_repeatedly()的方法,它承诺以固定的间隔调用回调。此方法已被撤回,因为此类函数的设计过度指定。一方面,简单的计时器循环可以使用回调轻松模拟,该回调使用call_later()重新调度自身;也很容易编写包含循环和sleep()调用(模块中的顶级函数,见下文)的协程。另一方面,由于精确计时的复杂性,这里有许多未意识到的陷阱和陷阱(参见PEP 418),并且不同的用例在边缘情况下需要不同的行为。不可能为此目的提供一个在所有情况下都万无一失的API,因此认为最好让应用程序设计人员自行决定实现哪种计时器循环。
线程交互
call_soon_threadsafe(callback, *args)。类似于call_soon(callback, *args),但是当从另一个线程调用时,如果事件循环因等待I/O而被阻塞,则会解除阻塞事件循环。返回一个Handle。这是*唯一*一个可以从另一个线程安全调用的方法。(为了以线程安全的方式在稍后调度回调,您可以使用loop.call_soon_threadsafe(loop.call_later, when, callback, *args)。)注意:从信号处理程序调用此方法不安全(因为它可能使用锁)。事实上,没有API是信号安全的;如果您想处理信号,请使用下面描述的add_signal_handler()。run_in_executor(executor, callback, *args)。安排在执行器中调用callback(*args)(参见PEP 3148)。成功时返回一个asyncio.Future实例,其结果是该调用的返回值。这等价于wrap_future(executor.submit(callback, *args))。如果executor为None,则使用set_default_executor()设置的默认执行器。如果尚未设置默认执行器,则会创建一个具有默认线程数的ThreadPoolExecutor并将其设置为默认执行器。(默认实现在此情况下使用5个线程。)set_default_executor(executor)。设置run_in_executor()使用的默认执行器。参数必须是PEP 3148Executor实例或None,以重置默认执行器。
另请参阅Futures部分中描述的wrap_future()函数。
互联网名称查找
如果您想连接或绑定套接字到某个地址而没有名称查找阻塞的风险,这些方法非常有用。它们通常由create_connection()、create_server()或create_datagram_endpoint()隐式调用。
getaddrinfo(host, port, family=0, type=0, proto=0, flags=0)。类似于socket.getaddrinfo()函数,但返回一个Future。Future成功时的结果将是一个与socket.getaddrinfo()返回格式相同的列表,即一个(address_family, socket_type, socket_protocol, canonical_name, address)列表,其中address对于IPv4地址是2元组(ipv4_address, port),对于IPv6地址是4元组(ipv6_address, port, flow_info, scope_id)。如果family参数为零或未指定,则返回的列表可能包含IPv4和IPv6地址的混合;否则,返回的地址受family值限制(proto和flags类似)。默认实现使用run_in_executor()调用socket.getaddrinfo(),但其他实现可以选择实现自己的DNS查找。可选参数*必须*指定为关键字参数。注意:允许实现仅实现
socket.getaddrinfo()接口的子集;例如,它们可能不支持符号端口名称,或者它们可能忽略或不完全实现type、proto和flags参数。但是,如果忽略type和proto,则传入的参数值应不变地复制到返回元组的socket_type和socket_protocol元素中。(不能忽略family,因为IPv4和IPv6地址必须以不同的方式查找。允许的family值只有socket.AF_UNSPEC(0)、socket.AF_INET和socket.AF_INET6,后者仅当平台定义时。)getnameinfo(sockaddr, flags=0)。类似于socket.getnameinfo()但返回一个Future。Future成功时的结果将是一个元组(host, port)。getaddrinfo()的实现备注同样适用于此。
互联网连接
这些是用于管理互联网连接的高级接口。建议使用它们而不是相应的低级接口,因为它们抽象了基于选择器和基于Proactor的事件循环之间的差异。
请注意,流连接的客户端和服务器端使用相同的传输和协议接口。然而,数据报端点使用不同的传输和协议接口。
create_connection(protocol_factory, host, port, <options>)。创建到给定互联网主机和端口的流连接。这是一个通常从连接的客户端调用的任务。它创建一个依赖于实现的双向流传输来表示连接,然后调用protocol_factory()来实例化(或检索)用户的协议实现,最后将两者绑定在一起。(传输和协议的定义见下文。)用户的协议实现是通过不带参数调用protocol_factory()来创建或检索的(*)。协程成功时的结果是(transport, protocol)对;如果失败阻止了成功连接的创建,则会引发适当的异常。请注意,当协程完成时,协议的connection_made()方法尚未被调用;这将在连接握手完成后发生。(*) 没有要求
protocol_factory是一个类。如果您的协议类需要将其特定参数传递给其构造函数,您可以使用lambda。您还可以传递一个微不足道的lambda,它返回一个以前构造的Protocol实例。所有<options>都使用可选关键字参数指定
ssl:传递True以创建SSL/TLS传输(默认创建普通TCP传输)。或者传递一个ssl.SSLContext对象以覆盖要使用的默认SSL上下文对象。如果创建了默认上下文,则由实现来配置合理的默认值。参考实现当前使用PROTOCOL_SSLv23并设置OP_NO_SSLv2选项,调用set_default_verify_paths()并将verify_mode设置为CERT_REQUIRED。此外,无论何时上下文(默认或非默认)指定CERT_REQUIRED或CERT_OPTIONAL的verify_mode,如果提供了主机名,则在成功握手后立即调用ssl.match_hostname(peercert, hostname),如果这引发异常,则连接将关闭。(要避免此行为,请传入一个将verify_mode设置为CERT_NONE的SSL上下文。但这表示您不安全,并且容易受到例如中间人攻击。)family、proto、flags:要传递给getaddrinfo()的地址族、协议和标志。这些都默认为0,表示“未指定”。(套接字类型始终为SOCK_STREAM。)如果这些值中的任何一个未指定,getaddrinfo()方法将选择适当的值。注意:proto与高级协议概念或protocol_factory参数无关。sock:一个可选的套接字,用于替代使用host、port、family、proto和flags参数。如果给定此参数,host和port必须明确设置为None。local_addr:如果给定,则是一个(host, port)元组,用于将套接字本地绑定到。这很少需要,但在多宿主服务器上,您偶尔需要强制连接来自特定地址。这就是您如何做到这一点。主机和端口使用getaddrinfo()查找。server_hostname:此参数仅在使用SSL/TLS时相关;当ssl未设置时不应使用。当ssl已设置时,此参数设置或覆盖将要验证的主机名。默认情况下使用host参数的值。如果host为空,则没有默认值,您必须为server_hostname传递一个值。要禁用主机名验证(这是一个严重的安全风险),您必须在此处传递一个空字符串,并将ssl参数设置为verify_mode设置为ssl.CERT_NONE的ssl.SSLContext对象。
create_server(protocol_factory, host, port, <options>)。进入一个接受连接的服务循环。这是一个协程,一旦服务循环设置为服务就完成。返回值是一个Server对象,可用于以受控方式停止服务循环(参见下文)。如果指定地址允许IPv4和IPv6连接,则可以绑定多个套接字。每次接受连接时,会不带参数调用
protocol_factory(**)来创建协议,创建一个双向流传输来表示连接的网络侧,并通过调用protocol.connection_made(transport)将两者连接起来。(**) 有关
create_connection(),请参阅前面的脚注。然而,由于protocol_factory()会为每个新传入连接调用一次,它应该在每次调用时返回一个新的协议对象。所有<options>都使用可选关键字参数指定
ssl:传递一个ssl.SSLContext对象(或具有相同接口的对象)以覆盖要使用的默认SSL上下文对象。(与create_connection()不同,在此处传递True没有意义——需要SSLContext对象来指定证书和密钥。)backlog:要传递给listen()调用的backlog值。默认值取决于实现;在默认实现中,默认值为100。reuse_address:是否在套接字上设置SO_REUSEADDR选项。默认在UNIX上为True,在Windows上为False。family,flags:要传递给getaddrinfo()的地址族和标志。family默认为AF_UNSPEC;flags默认为AI_PASSIVE。(套接字类型始终为SOCK_STREAM;套接字协议始终设置为0,让getaddrinfo()选择。)sock:一个可选的套接字,用于替代使用host、port、family和flags参数。如果给定此参数,host和port必须明确设置为None。
create_datagram_endpoint(protocol_factory, local_addr=None, remote_addr=None, <options>)。创建用于发送和接收数据报(通常是UDP数据包)的端点。由于数据报流量的性质,没有单独的调用来设置客户端和服务器端,因为通常单个端点既充当客户端又充当服务器。这是一个协程,成功时返回一个(transport, protocol)对,失败时引发异常。如果协程成功返回,当收到数据报或套接字关闭时,传输将调用协议上的回调;协议负责调用协议上的方法来发送数据报。返回的传输是一个DatagramTransport。返回的协议是一个DatagramProtocol。这些将在稍后描述。强制位置参数
protocol_factory:一个类或工厂函数,将不带参数精确调用一次,以构造要返回的协议对象。数据报传输和协议之间的接口如下所述。
可以按位置或作为关键字参数指定的可选参数
local_addr:一个可选元组,指示套接字将绑定到的地址。如果给定,这必须是一个(host, port)对。它将被传递给getaddrinfo()进行解析,结果将传递给创建的套接字的bind()方法。如果getaddrinfo()返回多个地址,它们将依次尝试。如果省略,则不会进行bind()调用。remote_addr:一个可选元组,指示套接字将“连接”到的地址。(由于没有数据报连接这种东西,这只指定了传出数据报目标地址的默认值。)如果给定,这必须是一个(host, port)对。它将被传递给getaddrinfo()进行解析,结果将与创建的套接字一起传递给sock_connect()。如果getaddrinfo()返回多个地址,它们将依次尝试。如果省略,则不会进行sock_connect()调用。
所有<options>都使用可选关键字参数指定
family、proto、flags:要传递给getaddrinfo()的地址族、协议和标志。这些都默认为0,表示“未指定”。(套接字类型始终为SOCK_DGRAM。)如果这些值中的任何一个未指定,getaddrinfo()方法将选择适当的值。
请注意,如果
local_addr和remote_addr都存在,则将尝试所有具有匹配地址族的本地和远程地址组合。
封装的套接字方法
以下用于在套接字上执行异步I/O的方法不适用于一般用途。它们主要用于通过ProactorEventLoop类使用IOCP的传输实现。但是,它们很容易为其他事件循环类型实现,因此没有理由不要求它们。套接字参数必须是非阻塞套接字。
sock_recv(sock, n)。从套接字sock接收最多n个字节。返回一个Future,成功时其结果将是一个字节对象。sock_sendall(sock, data)。将字节data发送到套接字sock。返回一个Future,成功时其结果将是None。注意:此名称使用sendall而不是send,以反映此方法的语义和签名与标准库套接字方法sendall()而不是send()相同。sock_connect(sock, address)。连接到给定地址。返回一个Future,成功时其结果将是None。sock_accept(sock)。接受来自套接字的连接。套接字必须处于监听模式并绑定到某个地址。返回一个Future,成功时其结果将是一个元组(conn, peer),其中conn是一个已连接的非阻塞套接字,peer是对等地址。
I/O 回调
这些方法主要用于与选择器一起工作的传输实现。它们由SelectorEventLoop实现,但不由ProactorEventLoop实现。自定义事件循环实现可能实现也可能不实现它们。
下面的fd参数可以是整数文件描述符,也可以是具有fileno()方法的“文件类”对象,该方法封装了整数文件描述符。并非所有文件类对象或文件描述符都是可接受的。套接字(和套接字文件描述符)始终被接受。在Windows上不支持其他类型。在UNIX上,也支持管道和可能的tty设备,但不支持磁盘文件。具体支持哪些特殊文件类型可能因平台和选择器实现而异。(实验表明,OS X上至少有一种伪tty,它受select和poll支持,但不受kqueue支持:Emacs shell窗口使用它。)
add_reader(fd, callback, *args)。安排当文件描述符fd被认为准备好读取时调用callback(*args)。再次为同一文件描述符调用add_reader()意味着为同一文件描述符调用remove_reader()。add_writer(fd, callback, *args)。类似于add_reader(),但注册用于写入而不是读取的回调。remove_reader(fd)。取消文件描述符fd的当前读取回调(如果已设置)。如果文件描述符当前未设置回调,则此操作为空操作并返回False。否则,它会移除回调安排并返回True。remove_writer(fd)。这与add_writer()的关系类似于remove_reader()与add_reader()的关系。
管道和子进程
这些方法在UNIX上的SelectorEventLoop和Windows上的ProactorEventLoop中受支持。
用于管道和子进程的传输和协议与用于常规流连接的传输和协议不同。这些将在稍后描述。
以下每个方法都有一个protocol_factory参数,类似于create_connection();此参数将不带参数精确调用一次,以构造要返回的协议对象。
每个方法都是一个协程,成功时返回一个(transport, protocol)对,失败时引发异常。
connect_read_pipe(protocol_factory, pipe):从封装UNIX管道读取端的类文件对象创建单向流连接,该管道必须处于非阻塞模式。返回的传输是ReadTransport。connect_write_pipe(protocol_factory, pipe):从封装UNIX管道写入端的类文件对象创建单向流连接,该管道必须处于非阻塞模式。返回的传输是WriteTransport;它没有任何与读取相关的方法。返回的协议是BaseProtocol。subprocess_shell(protocol_factory, cmd, <options>):从cmd创建一个子进程,它是一个使用平台“shell”语法的字符串。这类似于标准库subprocess.Popen()类,调用时带shell=True。其余参数和返回值将在下面描述。subprocess_exec(protocol_factory, *args, <options>):从一个或多个字符串参数创建子进程,其中第一个字符串指定要执行的程序,其余字符串指定程序的参数。(因此,假设它是一个Python脚本,这些字符串参数共同构成了程序的sys.argv值。)这类似于标准库subprocess.Popen()类在调用时设置shell=False并将字符串列表作为第一个参数;然而,Popen()接受一个字符串列表作为单个参数,而subprocess_exec()接受多个字符串参数。其余参数和返回值将在下面描述。
除了指定要执行程序的方式不同,这两个subprocess_*()方法行为相同。返回的传输是SubprocessTransport,它与常见的双向流传输接口不同。返回的协议是SubprocessProtocol,它也有自定义接口。
所有<options>都使用可选关键字参数指定
stdin:一个文件类对象,表示将使用connect_write_pipe()连接到子进程标准输入流的管道,或者常量subprocess.PIPE(默认)。默认情况下,将创建一个新管道并连接。stdout:一个文件类对象,表示将使用connect_read_pipe()连接到子进程标准输出流的管道,或者常量subprocess.PIPE(默认)。默认情况下,将创建一个新管道并连接。stderr:一个文件类对象,表示将使用connect_read_pipe()连接到子进程标准错误流的管道,或者常量subprocess.PIPE(默认)或subprocess.STDOUT。默认情况下,将创建一个新管道并连接。当指定subprocess.STDOUT时,子进程的标准错误流将连接到与标准输出流相同的管道。bufsize:创建管道时使用的缓冲区大小;这会传递给subprocess.Popen()。在默认实现中,它默认为零,在Windows上它必须为零;这些默认值与subprocess.Popen()不同。executable、preexec_fn、close_fds、cwd、env、startupinfo、creationflags、restore_signals、start_new_session、pass_fds:这些可选参数直接传递给subprocess.Popen(),不进行任何解释。
信号回调
这些方法仅在UNIX上支持。
add_signal_handler(sig, callback, *args)。每当接收到信号sig时,安排调用callback(*args)。为同一信号指定另一个回调将替换之前的处理程序(每个信号只能有一个活动处理程序)。sig必须是signal模块中定义的有效信号编号。如果信号无法处理,则会引发异常:如果不是有效信号或它是不可捕获的信号(例如SIGKILL),则为ValueError;如果此特定事件循环实例无法处理信号(因为信号是进程全局的,只有与主线程关联的事件循环才能处理信号),则为RuntimeError。remove_signal_handler(sig)。如果设置了信号sig的处理程序,则将其移除。引发与add_signal_handler()相同的异常(除了对于不可捕获的信号,它可能返回False而不是引发RuntimeError)。如果处理程序成功移除,则返回True;如果没有设置处理程序,则返回False。
注意:如果这些方法在静态上已知不受支持,它们可能会引发NotImplementedError而不是RuntimeError。
回调的互斥
事件循环应强制执行回调的互斥,即它绝不应在先前回调仍在运行时启动回调。这应适用于所有类型的回调,无论它们是使用call_soon()、call_later()、call_at()、call_soon_threadsafe()、add_reader()、add_writer()或add_signal_handler()进行调度的。
异常
Python中有两类异常:一类派生自Exception类,另一类派生自BaseException。派生自Exception的异常通常会被捕获并适当处理;例如,它们会由Futures传递,并在回调中发生时被记录和忽略。
然而,仅派生自BaseException的异常通常不会被捕获,并且通常会导致程序因回溯而终止。在某些情况下,它们会被捕获并重新引发。(此类别中的示例包括KeyboardInterrupt和SystemExit;将它们与大多数其他异常同等对待通常是不明智的。)
事件循环将后一类传递到其*异常处理程序*中。这是一个接受*上下文*字典作为参数的回调。
def exception_handler(context):
...
context可能包含许多不同的键,但其中几个被广泛使用
'message':错误消息。'exception':异常实例;如果没有异常,则为None。'source_traceback':表示错误中涉及的对象创建时堆栈的字符串列表。'handle_traceback':表示错误中涉及的句柄创建时堆栈的字符串列表。
循环具有以下与异常处理相关的方法
get_exception_handler()返回为该循环注册的当前异常处理程序。set_exception_handler(handler)设置异常处理程序。default_exception_handler(context)此循环实现的*默认*异常处理程序。call_exception_handler(context)将*context*传递给已注册的异常处理程序。这允许第三方库统一处理未捕获的异常。如果默认设置未被显式
set_exception_handler()调用覆盖,则循环将使用default_exception_handler()。
调试模式
默认情况下,循环在*发布*模式下运行。应用程序可以启用*调试*模式,以牺牲一些性能为代价,获取更好的错误报告。
在调试模式下,会启用许多额外的检查,例如
- 期货/任务中未处理的异常可提供源回溯。
- 循环检查慢速回调以检测意外的I/O阻塞。
loop.slow_callback_duration属性控制两个*yield点*之间允许的最大执行时间,超过该时间会报告慢回调。默认值为0.1秒;可以通过赋值来更改。
有两个与调试模式相关的方法
get_debug()如果启用了*调试*模式,则返回True,否则返回False。set_debug(enabled)如果参数为True,则启用*调试*模式。
如果定义了PYTHONASYNCIODEBUG*环境变量*且不为空,则调试模式会自动启用。
句柄
用于注册一次性回调的各种方法(call_soon()、call_later()、call_at()和call_soon_threadsafe())都返回一个表示注册的对象,可用于取消回调。此对象称为Handle。Handle是不透明的,只有一个公共方法
cancel():取消回调。
请注意,add_reader()、add_writer()和add_signal_handler()不返回Handle。
服务器
create_server()方法返回一个Server实例,它封装用于接受请求的套接字(或其他网络对象)。该类有两个公共方法
close():关闭服务。这会停止接受新请求,但不会取消已接受并正在处理的请求。wait_closed():一个协程,它会阻塞直到服务关闭且所有已接受的请求都已处理完毕。
Futures
这里的asyncio.Future类在设计上与PEP 3148中指定的concurrent.futures.Future类相似,但存在细微差异。除非明确提及concurrent.futures.Future,否则本PEP中提及的Future或futures都应理解为指asyncio.Future。支持的公共API如下,并指出了与PEP 3148的差异:
cancel()。如果Future已经完成(或已取消),则不执行任何操作并返回False。否则,它会尝试取消Future并返回True。如果取消尝试成功,最终Future的状态将变为已取消(因此cancelled()将返回True),并且回调将被调度。对于普通Future,取消总是立即成功;但对于任务(见下文),任务可能会忽略或延迟取消尝试。cancelled()。如果Future成功取消,则返回True。done()。如果Future已完成,则返回True。请注意,已取消的Future也被视为已完成(此处及所有情况)。result()。返回使用set_result()设置的结果,或引发使用set_exception()设置的异常。如果取消,则引发CancelledError。与PEP 3148的区别:此方法没有超时参数,也*不*等待;如果future尚未完成,它会引发异常。exception()。如果使用set_exception()设置了异常,则返回异常,否则如果使用set_result()设置了结果,则返回None。如果取消,则引发CancelledError。与PEP 3148的区别:此方法没有超时参数,也*不*等待;如果future尚未完成,它会引发异常。add_done_callback(fn)。添加一个回调,在Future完成(或取消)时运行。如果Future已经完成(或取消),则使用call_soon()调度回调。与PEP 3148的区别:回调绝不会立即调用,并且始终在调用者的上下文中调用——通常是线程。你可以将其视为通过call_soon()调用回调。请注意,为了与PEP 3148匹配,回调(与本PEP中定义的所有其他回调不同,并忽略下文“回调风格”一节的约定)始终以单个参数(Future对象)调用。(严格序列化使用call_soon()调度的回调的动机也适用于此。)remove_done_callback(fn)。从回调列表中移除参数。此方法未由PEP 3148定义。参数必须与传递给add_done_callback()的参数相等(使用==)。返回回调被移除的次数。set_result(result)。Future不得已经完成(或取消)。此操作使Future完成并调度回调。与PEP 3148的区别:这是一个公共API。set_exception(exception)。Future不得已经完成(或取消)。此操作使Future完成并调度回调。与PEP 3148的区别:这是一个公共API。
不支持内部方法set_running_or_notify_cancel();无法设置运行状态。同样,也不支持方法running()。
定义了以下异常
InvalidStateError。当Future未处于可接受方法调用的状态时引发(例如,在已完成的Future上调用set_result(),或在尚未完成的Future上调用result())。InvalidTimeoutError。当给定非零timeout参数时,由result()和exception()引发。CancelledError。concurrent.futures.CancelledError的别名。当在已取消的Future上调用result()或exception()时引发。TimeoutError。concurrent.futures.TimeoutError的别名。可能由run_until_complete()引发。
Future在创建时与事件循环关联。
concurrent.futures包中的wait()和as_completed()函数不接受asyncio.Future对象。但是,有类似的API asyncio.wait()和asyncio.as_completed(),如下所述。
asyncio.Future对象在协程中使用yield from表达式时是可接受的。这是通过Future上的__iter__()接口实现的。请参阅下面的“协程和调度器”一节。
当Future被垃圾回收时,如果它有一个关联的异常但从未调用过result()或exception(),则会记录该异常。(当协程使用yield from等待Future时,一旦协程恢复,就会调用该Future的result()方法。)
将来(双关语),我们可能会统一asyncio.Future和concurrent.futures.Future,例如通过向后者添加一个与yield from一起使用的__iter__()方法。为了防止通过调用尚未完成的Future上的result()等意外阻塞事件循环,阻塞操作可能会检测当前线程中是否存在活动的事件循环并引发异常。然而,当前的PEP致力于不依赖Python 3.3之外的任何内容,因此暂时不考虑对concurrent.futures.Future进行更改。
有一些与Future相关的公共函数
asyncio.async(arg)。此函数接受一个参数,该参数可以是协程对象或Future(即,任何可与yield from一起使用的东西),并返回一个Future。如果参数是Future,则原样返回;如果它是协程对象,则将其包装在一个Task中(请记住Task是Future的子类)。asyncio.wrap_future(future)。这接受一个PEP 3148 Future(即concurrent.futures.Future的实例)并返回一个与事件循环兼容的Future(即asyncio.Future实例)。
传输
传输和协议深受Twisted和PEP 3153的影响。用户很少实现或实例化传输——相反,事件循环提供实用方法来设置传输。
传输与协议协同工作。协议通常在不知道或不关心所使用的确切传输类型的情况下编写,并且传输可以与各种协议一起使用。例如,HTTP客户端协议实现可以与普通套接字传输或SSL/TLS传输一起使用。普通套接字传输除了HTTP之外还可以与许多不同的协议一起使用(例如SMTP、IMAP、POP、FTP、IRC、SPDY)。
最常见的传输类型是双向流传输。还有单向流传输(用于管道)和数据报传输(由create_datagram_endpoint()方法使用)。
所有传输的方法
get_extra_info(name, default=None)。这是一个包罗万象的方法,用于返回有关传输的特定于实现的信息。第一个参数是要检索的额外字段的名称。可选的第二个参数是要返回的默认值。请查阅实现文档以了解支持的额外字段名称。对于不支持的名称,始终返回默认值。
双向流传输
双向流传输是套接字或类似事物(例如,一对UNIX管道或SSL/TLS连接)之上的抽象。
大多数连接具有不对称性:客户端和服务器通常具有非常不同的角色和行为。因此,传输和协议之间的接口也是不对称的。从协议的角度来看,*写入*数据是通过调用传输对象上的write()方法完成的;这会缓冲数据并立即返回。但是,传输在*读取*数据中扮演更积极的角色:每当从套接字(或其他数据源)读取一些数据时,传输都会调用协议的data_received()方法。
尽管如此,客户端和服务器之间使用的双向流传输和协议接口是相同的,因为客户端和服务器之间的连接本质上是一对流,每个方向一个。
双向流传输具有以下公共方法
write(data)。写入一些字节。参数必须是字节对象。返回None。传输可以自由地缓冲字节,但最终必须将字节传输到另一端的实体,并且必须保持流行为。也就是说,t.write(b'abc'); t.write(b'def')等价于t.write(b'abcdef'),也等价于t.write(b'a') t.write(b'b') t.write(b'c') t.write(b'd') t.write(b'e') t.write(b'f')
writelines(iterable)。等价于for data in iterable: self.write(data)
write_eof()。关闭连接的写入端。不允许后续调用write()。一旦所有缓冲数据传输完毕,传输会向另一端发出信号,表示不会再接收到数据。有些协议不支持此操作;在这种情况下,调用write_eof()将引发异常。(注意:此方法以前称为half_close(),但除非你已经知道它的用途,否则该名称并未指明关闭的是*哪*一端。)can_write_eof()。如果协议支持write_eof(),则返回True,否则返回False。(此方法通常返回一个固定值,该值仅取决于特定的传输类,而不取决于传输对象的状态。它之所以需要,是因为某些协议在write_eof()不可用时需要改变其行为。例如,在HTTP中,要发送大小未知的数据,通常使用write_eof()指示数据的结束;然而,SSL/TLS不支持此功能,在这种情况下,HTTP协议实现必须使用“分块”传输编码。但如果数据大小事先已知,则两种情况下的最佳方法都是使用Content-Length头部。)get_write_buffer_size()。返回传输写入缓冲区的当前大小(以字节为单位)。这仅了解传输显式管理的写入缓冲区;网络堆栈或其他网络层中的缓冲未报告。set_write_buffer_limits(high=None, low=None)。为流控制设置高水位和低水位限制。这两个值控制何时调用协议的
pause_writing()和resume_writing()方法。如果指定,低水位限制必须小于或等于高水位限制。两个值都不能为负数。默认值是实现定义的。如果只给出了高水位限制,则低水位限制默认为小于或等于高水位限制的实现定义值。将高水位设置为零也会将低水位强制为零,并导致在缓冲区变为非空时调用
pause_writing()。将低水位设置为零会导致仅在缓冲区为空时调用resume_writing()。对任何一个限制使用零通常不是最优的,因为它减少了并发进行I/O和计算的机会。pause_reading()。暂停向协议传递数据,直到后续调用resume_reading()。在pause_reading()和resume_reading()之间,协议的data_received()方法将不会被调用。resume_reading()。通过data_received()重新开始向协议传递数据。请注意,“暂停”是一个二进制状态——pause_reading()只能在传输未暂停时调用,而resume_reading()只能在传输暂停时调用。close()。断开与另一端实体的连接。通过write()缓冲的任何数据都将在连接实际关闭之前(最终)传输。协议的data_received()方法将不会再次被调用。一旦所有缓冲数据被刷新,协议的connection_lost()方法将被调用,参数为None。请注意,此方法不会等待所有这些事情发生。abort()。立即断开连接。传输中仍缓冲的任何数据都将被丢弃。很快,协议的connection_lost()方法将被调用,参数为None。
单向流传输
写入流传输支持双向流传输中描述的write()、writelines()、write_eof()、can_write_eof()、close()和abort()方法。
读取流传输支持双向流传输中描述的pause_reading()、resume_reading()和close()方法。
写入流传输仅调用其关联协议上的connection_made()和connection_lost()。
读取流传输可以调用下面“协议”一节中指定的所有协议方法(即前两个方法加上data_received()和eof_received())。
数据报传输
数据报传输具有以下方法
sendto(data, addr=None)。发送一个数据报(字节对象)。可选的第二个参数是目标地址。如果省略,则在创建此传输的create_datagram_endpoint()调用中必须指定remote_addr。如果存在且指定了remote_addr,则它们必须匹配。该(数据,地址)对可以立即发送或缓冲。返回值为None。abort()。立即关闭传输。缓冲数据将被丢弃。close()。关闭传输。缓冲数据将异步传输。
数据报传输在其关联协议对象上调用以下方法:connection_made()、connection_lost()、error_received()和datagram_received()。(这些方法名称中的“连接”有点误称,但概念仍然存在:connection_made()表示代表端点的传输已创建,而connection_lost()表示传输已关闭。)
子进程传输
子进程传输具有以下方法
get_pid()。返回子进程的进程ID。get_returncode()。如果进程已退出,则返回进程返回码;否则返回None。get_pipe_transport(fd)。返回与参数对应的管道传输(单向流传输),参数应为0、1或2,分别代表子进程的标准输入、标准输出或标准错误。如果不存在此类管道传输,则返回None。对于标准输入,这是一个写入传输;对于标准输出和标准错误,这是一个读取传输。您必须使用此方法才能获得可用于写入子进程标准输入的传输。send_signal(signal)。向子进程发送信号。terminate()。终止子进程。kill()。杀死子进程。在Windows上,这是terminate()的别名。close()。这是terminate()的别名。
请注意,send_signal()、terminate()和kill()封装了标准库subprocess模块中的相应方法。
协议
协议总是与传输结合使用。虽然提供了一些常用协议(例如,不错但未必优秀的HTTP客户端和服务器实现),但大多数协议将由用户代码或第三方库实现。
与传输类似,我们将流协议、数据报协议以及其他自定义协议区分开来。最常见的协议类型是双向流协议。(没有单向协议。)
流协议
(双向)流协议必须实现以下方法,这些方法将由传输调用。将它们视为回调,它们始终由事件循环在正确的上下文中调用。(请参阅上面的“上下文”部分。)
connection_made(transport)。表示传输已就绪并连接到另一端的实体。协议应该保存传输引用作为实例变量(以便以后可以调用其write()和其他方法),并可以在此时写入初始问候语或请求。data_received(data)。传输已从连接中读取了一些字节。参数始终是非空字节对象。对于通过此方式传递的数据的最小或最大大小没有保证。p.data_received(b'abcdef')应被视为完全等价于p.data_received(b'abc') p.data_received(b'def')
eof_received()。当另一端调用write_eof()(或类似方法)时调用此方法。如果此方法返回假值(包括None),则传输将自行关闭。如果它返回真值,则关闭传输由协议负责。但是,对于SSL/TLS连接,此方法被忽略,因为TLS标准要求在收到“关闭警报”后不再发送数据并关闭连接。默认实现返回
None。pause_writing()。要求协议暂时停止向传输写入数据。响应此请求是可选的,但如果您继续写入,传输的缓冲区可能会无限制地增长。调用此方法时的缓冲区大小可以通过传输的set_write_buffer_limits()方法控制。resume_writing()。通知协议可以再次安全地开始向传输写入数据。请注意,这可以直接由传输的write()方法调用(而不是使用call_soon()间接调用),以便协议在write()返回后立即知道其暂停状态。connection_lost(exc)。传输已关闭或中止,已检测到另一端已干净地关闭连接,或遇到意外错误。在前三种情况下,参数为None;对于意外错误,参数是导致传输放弃的异常。
以下表格显示了基本调用的顺序和多重性
connection_made()– 恰好一次data_received()– 零次或多次eof_received()– 最多一次connection_lost()– 恰好一次
对pause_writing()和resume_writing()的调用成对出现,且仅在#1和#4之间。这些对不会嵌套。最终的resume_writing()调用可能会被省略;即,暂停的连接可能会丢失并且永远不会恢复。
数据报协议
数据报协议具有与流协议相同签名的connection_made()和connection_lost()方法。(如数据报传输部分所述,我们更喜欢略显奇怪的命名法,而不是定义不同的方法名称来指示套接字的打开和关闭。)
此外,它们还有以下方法
datagram_received(data, addr)。表示从远程地址addr(IPv4 2元组或IPv6 4元组)接收到数据报data(字节对象)。error_received(exc)。表示发送或接收操作引发了OSError异常。由于数据报错误可能是暂时的,协议可以自行决定是否调用传输的close()方法来关闭端点。
以下图表表示了调用的顺序和多重性
connection_made()– 恰好一次datagram_received()、error_received()– 零次或多次connection_lost()– 恰好一次
子进程协议
子进程协议具有与流协议相同签名的connection_made()、connection_lost()、pause_writing()和resume_writing()方法。此外,它们还有以下方法
pipe_data_received(fd, data)。当子进程向其标准输出或标准错误写入数据时调用。fd是文件描述符(标准输出为1,标准错误为2)。data是bytes对象。pipe_connection_lost(fd, exc)。当子进程关闭其标准输入、标准输出或标准错误时调用。fd是文件描述符。exc是异常或None。process_exited()。当子进程退出时调用。要检索退出状态,请使用传输的get_returncode()方法。
请注意,根据子进程的行为,process_exited()可能在pipe_connection_lost()之前或之后被调用。例如,如果子进程创建一个子子进程,该子子进程共享其标准输入/输出/错误,然后子进程本身退出,则process_exited()可能在所有管道仍打开时被调用。另一方面,当子进程关闭其标准输入/输出/错误但未退出时,pipe_connection_lost()可能会为所有三个管道调用,而process_exited()未被调用。如果(更常见的情况是)子进程退出并因此隐式关闭所有管道,则调用顺序未定义。
回调风格
大多数接受回调的接口也接受位置参数。例如,要安排foo("abc", 42)很快被调用,您可以调用loop.call_soon(foo, "abc", 42)。要调度调用foo(),请使用loop.call_soon(foo)。此约定大大减少了典型回调编程中所需的小型lambda的数量。
此约定特别*不*支持关键字参数。关键字参数用于传递有关回调的可选额外信息。这允许API的优雅演变,而无需担心关键字是否对某个被调用者有意义。如果您有一个*必须*使用关键字参数调用的回调,您可以使用lambda。例如
loop.call_soon(lambda: foo('abc', repeat=42))
协程和调度器
这是一个独立的高级部分,因为其状态与事件循环接口不同。协程的使用是可选的,只使用回调编写代码完全可以。另一方面,调度器/协程API只有一个实现,如果您正在使用协程,那么您正在使用的就是它。
协程
协程是遵循特定约定的生成器。为了文档目的,所有协程都应使用@asyncio.coroutine装饰,但这不能严格强制执行。
协程使用PEP 380中引入的yield from语法,而不是原始的yield语法。
“协程”一词,如同“生成器”一词,用于两个不同(但相关)的概念
- 定义协程的函数(用
asyncio.coroutine装饰的函数定义)。如果需要区分,我们将其称为*协程函数*。 - 通过调用协程函数获得的对象。此对象表示最终将完成的计算或I/O操作(通常是两者的组合)。如果需要区分,我们将其称为*协程对象*。
协程可以做的事情
result = yield from future– 暂停协程直到Future完成,然后返回Future的结果,或引发异常,该异常将被传播。(如果Future被取消,它将引发CancelledError异常。)请注意,任务是Future,关于Future的一切也适用于任务。result = yield from coroutine– 等待另一个协程产生结果(或引发异常,该异常将被传播)。coroutine表达式必须是对另一个协程的*调用*。return expression– 使用yield from向等待此协程的协程产生结果。raise exception– 使用yield from在等待此协程的协程中引发异常。
调用协程并不会立即启动其代码运行——它只是一个生成器,调用返回的协程对象实际上是一个生成器对象,在您遍历它之前不会执行任何操作。对于协程对象,有两种基本方法可以启动它运行:从另一个协程中调用yield from coroutine(假设另一个协程已经在运行!),或将其转换为任务(见下文)。
协程(和任务)只能在事件循环运行时运行。
等待多个协程
为了等待多个协程或Future,提供了两个类似于concurrent.futures包中的wait()和as_completed() API
asyncio.wait(fs, timeout=None, return_when=ALL_COMPLETED)。这是一个协程,它等待fs给定的Future或协程完成。协程参数将被包装在任务中(见下文)。它返回一个Future,成功时其结果是两个Future集合的元组,(done, pending),其中done是已完成(或已取消)的原始Future(或包装的协程)的集合,pending是其余的,即仍未完成(或未取消)的Future。请注意,在timeout和return_when的默认情况下,done将始终为空列表。可选参数timeout和return_when具有与concurrent.futures.wait()相同的含义和默认值:timeout,如果不为None,则指定整个操作的超时;return_when指定何时停止。常量FIRST_COMPLETED、FIRST_EXCEPTION、ALL_COMPLETED的定义值和含义与PEP 3148中相同。ALL_COMPLETED(默认):等待所有Future完成(或直到超时发生)。FIRST_COMPLETED:等待至少一个Future完成(或直到超时发生)。FIRST_EXCEPTION:等待至少一个Future完成但未因设置异常而取消。(将已取消的Future从条件中排除令人惊讶,但PEP 3148就是这样做的。)
asyncio.as_completed(fs, timeout=None)。返回一个迭代器,其值为Future或协程;等待连续的值会等待fs集合中的下一个Future或协程完成,并返回其结果(或引发其异常)。可选参数timeout具有与concurrent.futures.wait()相同的含义和默认值:当超时发生时,迭代器返回的下一个Future在等待时将引发TimeoutError。使用示例:for f in as_completed(fs): result = yield from f # May raise an exception. # Use result.
注意:如果您不等待迭代器产生的值,您的
for循环可能不会取得进展(因为您不允许其他任务运行)。asyncio.wait_for(f, timeout)。这是一个方便的方法,用于等待单个协程或Future,并设置超时。当超时发生时,它会取消任务并引发TimeoutError。为了避免任务取消,请将其包装在shield()中。asyncio.gather(f1, f2, ...)。返回一个Future,它会等待所有参数(Future或协程)完成,并返回它们相应结果的列表。如果一个或多个参数被取消或引发异常,则返回的Future将被取消或其异常被设置(与第一个参数发生的情况匹配),并且其余参数将继续在后台运行。取消返回的Future不会影响参数。请注意,协程参数会使用asyncio.async()转换为Future。asyncio.shield(f)。等待一个Future,并保护它免受取消。这会返回一个Future,其结果或异常与参数完全相同;但是,如果返回的Future被取消,参数Future不受影响。此函数的一个用例是缓存HTTP服务器中处理请求的协程的查询结果。当客户端取消请求时,我们(可以说)希望查询缓存协程继续运行,以便当客户端重新连接时,查询结果(希望)已缓存。这可以这样写:
@asyncio.coroutine def handle_request(self, request): ... cached_query = self.get_cache(...) if cached_query is None: cached_query = yield from asyncio.shield(self.fill_cache(...)) ...
睡眠
协程asyncio.sleep(delay)在给定的时间延迟后返回。
任务
任务是一个管理独立运行协程的对象。任务接口与Future接口相同,实际上Task是Future的子类。当其协程返回或引发异常时,任务就完成了;如果它返回结果,则该结果成为任务的结果,如果它引发异常,则该异常成为任务的异常。
取消尚未完成的任务会向协程抛出asyncio.CancelledError异常。如果协程没有捕获此异常(或者它重新引发了它),则任务将被标记为已取消(即cancelled()将返回True);但如果协程以某种方式捕获并忽略了该异常,它可能会继续执行(并且cancelled()将返回False)。
任务对于协程和基于回调的框架(如Twisted)之间的互操作也很有用。将协程转换为任务后,可以将回调添加到任务中。
要将协程转换为任务,请调用协程函数,并将生成的协程对象传递给loop.create_task()方法。您也可以为此目的使用asyncio.ensure_future()。
您可能会问,为什么不自动将所有协程转换为任务?@asyncio.coroutine装饰器可以做到这一点。但是,在一个协程调用另一个协程(依此类推)的情况下,这将大大降低速度,因为切换到“裸”协程的开销比切换到任务要小得多。
Task类派生自Future,并添加了新方法
current_task(loop=None)。一个*类方法*,返回事件循环中当前正在运行的任务。如果*loop*为None,该方法返回默认循环的当前任务。每个协程都在*任务上下文*中执行,无论是使用ensure_future()或loop.create_task()创建的Task,还是通过另一个协程使用yield from或await调用。当在协程*之外*调用时,例如在通过loop.call_later()调度的回调中,此方法返回None。all_tasks(loop=None)。一个*类方法*,返回循环中所有活动任务的集合。如果*loop*为None,则使用默认循环。
调度器
调度器没有公共接口。您通过使用yield from future和yield from task与其交互。实际上,没有单个对象代表调度器——其行为由Task和Future类仅使用事件循环的公共接口实现,因此它也适用于第三方事件循环实现。
便利工具
提供了一些函数和类,以简化基于流的基本客户端和服务器(如FTP或HTTP)的编写。它们是
asyncio.open_connection(host, port):EventLoop.create_connection()的包装器,它不要求您提供Protocol工厂或类。这是一个协程,返回一个(reader, writer)对,其中reader是StreamReader的实例,writer是StreamWriter的实例(两者均在下文描述)。asyncio.start_server(client_connected_cb, host, port):EventLoop.create_server()的包装器,它接受一个简单的回调函数而不是Protocol工厂或类。这是一个协程,返回一个Server对象,就像create_server()一样。每次接受客户端连接时,都会调用client_connected_cb(reader, writer),其中reader是StreamReader的实例,writer是StreamWriter的实例(两者均在下文描述)。如果client_connected_cb()返回的结果是一个协程,它会自动包装在一个Task中。StreamReader:一个提供类似于只读二进制流接口的类,不同之处在于各种读取方法都是协程。它通常由StreamReaderProtocol实例驱动。请注意,应该只有一个读取器。读取器的接口是readline():一个协程,它读取以'\n'结尾的文本行字节串,或者直到流结束,以先发生者为准。read(n):一个协程,它最多读取n个字节。如果省略n或n为负数,它会一直读取直到流结束。readexactly(n):一个协程,它精确读取n个字节,或者直到流结束,以先发生者为准。exception():返回已通过set_exception()在流上设置的异常,如果未设置异常则返回None。
驱动程序的接口是
feed_data(data):将data(一个bytes对象)追加到内部缓冲区。如果提供足够的数据以满足读取器的合同,这将解除阻塞的读取协程。feed_eof():发出缓冲区结束的信号。这将解除阻塞的读取协程。在此调用之后,不应再向读取器提供数据。set_exception(exc):在流上设置一个异常。所有后续的读取方法都将引发此异常。在此调用之后,不应再向读取器提供数据。
StreamWriter:一个提供类似于只写二进制流接口的类。它封装了一个传输。接口是传输接口的扩展子集:以下方法的行为与相应的传输方法相同:write()、writelines()、write_eof()、can_write_eof()、get_extra_info()、close()。请注意,写入方法_不是_协程(这与传输相同,但与StreamReader类不同)。以下方法是传输接口的附加方法drain():在写入大量数据后,应使用yield from调用此方法,以进行流控制。其预期用法如下:writer.write(data) yield from writer.drain()
请注意,这在技术上不是协程:它返回一个Future或一个空元组(两者都可以传递给
yield from)。此方法的使用是可选的。但是,当不使用它时,StreamWriter底层的传输内部缓冲区可能会充满写入器所写入的所有数据。如果应用程序对写入的数据量没有严格限制,它_应该_偶尔调用yield from drain()以避免传输缓冲区溢出。
StreamReaderProtocol:一种协议实现,用作双向流传输/协议接口与StreamReader和StreamWriter类之间的适配器。它充当特定StreamReader实例的驱动程序,响应各种协议回调,调用其feed_data()、feed_eof()和set_exception()方法。它还控制StreamWriter实例的drain()方法的行为。
同步
模仿threading模块中的锁、事件、条件和信号量已实现,可以通过导入asyncio.locks子模块来访问。模仿queue模块中的队列已实现,可以通过导入asyncio.queues子模块来访问。
总的来说,它们与其多线程对应的部分有很强的对应关系,但是,阻塞方法(例如锁的 acquire(),队列的 put() 和 get())是协程,并且没有提供超时参数(但是,您可以使用 asyncio.wait_for() 为阻塞调用添加超时)。
模块中的文档字符串提供了更完整的文档。
锁
asyncio.locks 提供了以下类。除了 Event 之外,所有这些类都可以结合 yield from 使用 with 语句来获取锁,并确保无论 with 块如何退出,锁都会被释放,如下所示:
with (yield from my_lock):
...
Lock:一个基本的互斥锁,具有方法acquire()(一个协程)、locked()和release()。Event:一个事件变量,具有方法wait()(一个协程)、set()、clear()和is_set()。Condition:一个条件变量,具有方法acquire()、wait()、wait_for(predicate)(均为协程)、locked()、release()、notify()和notify_all()。Semaphore:一个信号量,具有方法acquire()(一个协程)、locked()和release()。构造函数参数是初始值(默认为1)。BoundedSemaphore:一个有界信号量;它类似于Semaphore,但初始值也是最大值。
队列
asyncio.queues 提供了以下类和异常。
Queue:一个标准队列,具有方法get()、put()(均为协程)、get_nowait()、put_nowait()、empty()、full()、qsize()和maxsize()。PriorityQueue:Queue的子类,按优先级顺序(最低优先)检索条目。LifoQueue:Queue的子类,首先检索最近添加的条目。JoinableQueue:Queue的子类,具有task_done()和join()方法(后者是一个协程)。Empty,Full:当对空队列或满队列分别调用get_nowait()或put_nowait()时引发的异常。
杂项
日志记录
asyncio 包执行的所有日志记录都使用单个 logging.Logger 对象,即 asyncio.logger。要自定义日志记录,您可以使用此对象上的标准 Logger API。(但不要替换该对象。)
UNIX上的SIGCHLD处理
在子进程协议上高效实现 process_exited() 方法需要一个 SIGCHLD 信号处理程序。然而,信号处理程序只能在与主线程关联的事件循环上设置。为了支持从其他线程中运行的事件循环生成子进程,存在一种机制,允许在多个事件循环之间共享一个 SIGCHLD 处理程序。还有两个附加函数,asyncio.get_child_watcher() 和 asyncio.set_child_watcher(),以及事件循环策略上的相应方法。
有两种子进程监视器实现类,FastChildWatcher 和 SafeChildWatcher。两者都使用 SIGCHLD。SafeChildWatcher 类是默认使用的;当许多子进程同时存在时,它的效率不高。FastChildWatcher 类效率很高,但它可能会干扰其他不使用 asyncio 事件循环生成子进程的代码(无论是 C 代码还是 Python 代码)。如果您确定没有使用其他生成子进程的代码,要使用快速实现,请在主线程中运行以下代码:
watcher = asyncio.FastChildWatcher()
asyncio.set_child_watcher(watcher)
愿望清单
(大家一致认为这些功能是可取的,但在 Python 3.4 beta 1 发布时还没有实现,并且 Python 3.4 发布周期其余部分的特性冻结禁止在此晚期阶段添加它们。然而,它们有望在 Python 3.5 中添加,或许更早地在 PyPI 发布中添加。)
- 支持“启动 TLS”操作以将 TCP 套接字升级到 SSL/TLS。
已实现的旧愿望清单项目(但未由 PEP 指定)
- UNIX 域套接字。
- 每个循环的错误处理回调。
未解决的问题
(请注意,通过接受 PEP,这些问题实际上已根据现状得到解决。然而,PEP 的临时状态允许为 Python 3.5 修改这些决定。)
- 为什么
create_connection()和create_datagram_endpoint()有proto参数,而create_server()没有?为什么getaddrinfo()的 family、flag、proto 参数有时为零,有时是命名常量(其值也为零)? - 我们是否需要另一个查询方法来判断循环是否正在停止?
- Handle 更完整的公共 API?用例是什么?
- 一个调试 API?例如,记录大量信息,或记录异常情况(如队列填充速度快于排空速度),甚至回调函数耗时过长...
- 我们需要内省 API 吗?例如,给定文件描述符,询问读取回调。或者下一次计划的调用是什么时候。或者注册了回调的文件描述符列表。目前这些都需要使用内部实现。
- 我们需要更多套接字 I/O 方法吗?例如
sock_sendto()和sock_recvfrom(),也许还有其他类似pipe_read()的方法?我想用户可以自己编写(这不是火箭科学)。 - 我们可能需要 API 来控制各种超时。例如,我们可能希望限制 DNS 解析、连接、SSL/TLS 握手、空闲连接、关闭/关机甚至每个会话的时间。可能足以向某些方法添加
timeout关键字参数,其他超时可以通过巧妙地使用call_later()和Task.cancel()来实现。但是,某些操作可能需要默认超时,我们可能希望全局更改特定操作的默认值(即,每个事件循环)。
参考资料
- PEP 492 描述了
async/await的语义。 - PEP 380 描述了
yield from的语义。 - Greg Ewing 的
yield from教程:http://www.cosc.canterbury.ac.nz/greg.ewing/python/yield-from/yield_from.html - PEP 3148 描述了
concurrent.futures.Future。 - PEP 3153 虽然被拒绝,但有一篇很好的文章解释了分离传输和协议的必要性。
- PEP 418 讨论了时间保持问题。
- Tulip 仓库:http://code.google.com/p/tulip/
- PyPI:Python 包索引,网址为 http://pypi.python.org/
- Alyssa Coghlan 写了一篇不错的博客文章,其中包含了一些背景知识,关于异步 I/O 的不同方法、gevent 以及如何将 futures 与
while、for和with等结构结合使用:http://python-notes.boredomandlaziness.org/en/latest/pep_ideas/async_programming.html - 待定:Twisted、Tornado、ZeroMQ、pyftpdlib、libevent、libev、pyev、libuv、wattle 等相关部分的参考。
致谢
除了 PEP 3153,其影响还包括 PEP 380 和 Greg Ewing 关于 yield from 的教程、Twisted、Tornado、ZeroMQ、pyftpdlib 和 wattle(Steve Dower 的反建议)。我之前在 Google App Engine 的 NDB 库中异步支持方面的工作提供了一个重要的起点。
我非常感谢 2012 年 9 月至 12 月在 python-ideas 上以及此后在 python-tulip 上的多次讨论;与 Steve Dower 和 Dino Viehland 的 Skype 会议;与 Ben Darnell 的电子邮件交流和拜访;与 Niels Provos(libevent 的原始作者)的会见;以及与包括 Glyph、Brian Warner、David Reid 和 Duncan McGreggor 在内的多位 Twisted 开发人员的面对面会议(以及频繁的电子邮件交流)。
实施者包括 Eli Bendersky、Gustavo Carneiro (Gambit Research)、Saúl Ibarra Corretgé、Geert Jansen、A. Jesse Jiryu Davis、Nikolay Kim、Charles-François Natali、Richard Oudkerk、Antoine Pitrou、Giampaolo Rodolá、Andrew Svetlov 以及许多提交错误和/或修复的其他人。
我感谢 Antoine Pitrou 作为官方 PEP BDFL 提供的反馈。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-3156.rst