谈论async/await
和asyncio
不是一回事。第一个是基本的低级构造(协程),而第二个是使用这些构造的库。相反,没有单一的最终答案。
下面是如何的一般说明async/await
和asyncio
样库的工作。也就是说,可能还有其他的技巧(有...),但是除非您自己构建它们,否则它们是无关紧要的。除非您已经足够知道不必提出这样的问题,否则差异应该可以忽略不计。
1.坚果壳中的协程与子程序
就像子例程(函数,过程,...)一样,协程(生成器,...)是调用堆栈和指令指针的抽象:有执行代码段的堆栈,每个执行段都是特定的指令。
def
vs 的区别async def
只是为了清楚起见。实际的差别是return
对yield
。从此,await
或yield from
从单个调用到整个堆栈取不同。
1.1。子程序
子例程表示一个新的堆栈级别,用于保存局部变量,并且单次遍历其指令即可到达末尾。考虑这样的子例程:
def subfoo(bar):
qux = 3
return qux * bar
当您运行它时,这意味着
- 为
bar
和分配堆栈空间qux
- 递归执行第一个语句并跳转到下一个语句
- 一次
return
,将其值推入调用堆栈
- 清除堆栈(1.)和指令指针(2.)
值得注意的是,4.表示子例程始终以相同的状态开始。该功能本身专有的所有内容在完成后都会丢失。即使后面有说明,也无法恢复功能return
。
root -\
: \- subfoo --\
:/--<---return --/
|
V
1.2。协程作为持久子例程
协程就像一个子例程,但是可以在不破坏其状态的情况下退出。考虑这样的协程:
def cofoo(bar):
qux = yield bar # yield marks a break point
return qux
当您运行它时,这意味着
- 为
bar
和分配堆栈空间qux
- 递归执行第一个语句并跳转到下一个语句
- 一次
yield
,将其值压入调用堆栈,但存储堆栈和指令指针
- 一旦调用
yield
,恢复堆栈和指令指针并将参数推入qux
- 一次
return
,将其值推入调用堆栈
- 清除堆栈(1.)和指令指针(2.)
请注意,添加了2.1和2.2-协程可以在预定的位置挂起并恢复。这类似于在调用另一个子例程期间暂停子例程的方式。区别在于活动协程并不严格绑定到其调用堆栈。相反,悬挂的协程是单独的隔离堆栈的一部分。
root -\
: \- cofoo --\
:/--<+--yield --/
| :
V :
这意味着悬浮的协程可以在堆栈之间自由存储或移动。任何有权访问协程的调用堆栈都可以决定恢复它。
1.3。遍历调用栈
到目前为止,我们的协程仅在调用堆栈中yield
。子程序可以去和高达调用堆栈return
和()
。为了完整性,协程还需要一种机制来提升调用堆栈。考虑这样的协程:
def wrap():
yield 'before'
yield from cofoo()
yield 'after'
当您运行它时,这意味着它仍然像子例程一样分配堆栈和指令指针。当它挂起时,仍然就像存储一个子例程。
然而,yield from
确实两者。它挂起堆栈wrap
并运行指令cofoo
。请注意,它将wrap
保持挂起状态,直到cofoo
完全完成。每当cofoo
挂起或发送任何内容时,cofoo
都直接连接到调用堆栈。
1.4。协程一直向下
如建立的那样,yield from
允许将两个示波器连接到另一个中间示波器。递归应用时,这意味着堆栈的顶部可以连接到堆栈的底部。
root -\
: \-> coro_a -yield-from-> coro_b --\
:/ <-+------------------------yield ---/
| :
:\ --+-- coro_a.send----------yield ---\
: coro_b <-/
请注意,root
和coro_b
不知道对方。这使得协程比回调更干净:协程仍然像子例程一样建立在1:1关系上。协程将暂停并恢复其整个现有执行堆栈,直到常规调用点为止。
值得注意的是,root
可以恢复任意数量的协程。但是,它永远不能同时恢复多个。同一根的协程是并发的,但不是并行的!
1.5。Python的async
和await
到目前为止,该解释已明确使用生成器的yield
和yield from
词汇-基本功能相同。新的Python3.5语法async
并await
主要是为了清楚起见。
def foo(): # subroutine?
return None
def foo(): # coroutine?
yield from foofoo() # generator? coroutine?
async def foo(): # coroutine!
await foofoo() # coroutine!
return None
需要使用async for
and async with
语句,因为您将yield from/await
使用裸露的for
and with
语句断开链接。
2.简单事件循环的剖析
就一个协程本身而言,没有控制其他协程的概念。它只能对协程堆栈底部的调用者产生控制权。然后,此调用者可以切换到另一个协程并运行它。
几个协程的根节点通常是一个事件循环:在挂起时,协程会产生一个事件,并在该事件上恢复。反过来,事件循环能够有效地等待这些事件发生。这使它可以决定接下来要运行哪个协程,或在恢复之前如何等待。
这种设计意味着循环可以理解一组预定义的事件。几个协程await
相互配合,直到最终完成一个事件await
。该事件可以通过控制直接与事件循环通信yield
。
loop -\
: \-> coroutine --await--> event --\
:/ <-+----------------------- yield --/
| :
| : # loop waits for event to happen
| :
:\ --+-- send(reply) -------- yield --\
: coroutine <--yield-- event <-/
关键是协程暂停允许事件循环和事件直接通信。中间协程堆栈不需要任何有关运行哪个循环或事件如何工作的知识。
2.1.1。及时事件
要处理的最简单事件是到达某个时间点。这也是线程代码的基本块:线程重复sleep
s直到条件成立。但是,常规规则sleep
本身会阻止执行-我们希望其他协程不被阻止。相反,我们想告诉事件循环何时应恢复当前协程堆栈。
2.1.2。定义事件
事件只是我们可以识别的值-通过枚举,类型或其他标识。我们可以使用存储目标时间的简单类来定义它。除了存储事件信息之外,我们还可以await
直接允许一个类。
class AsyncSleep:
"""Event to sleep until a point in time"""
def __init__(self, until: float):
self.until = until
# used whenever someone ``await``s an instance of this Event
def __await__(self):
# yield this Event to the loop
yield self
def __repr__(self):
return '%s(until=%.1f)' % (self.__class__.__name__, self.until)
此类仅存储事件-它没有说明如何实际处理它。
唯一的特殊功能是__await__
- await
关键字寻找的内容。实际上,它是一个迭代器,但不适用于常规迭代机制。
2.2.1。等待事件
现在我们有了一个事件,协程对此有何反应?我们应该能够表达相当于sleep
由await
荷兰国际集团我们的活动。为了更好地了解发生了什么,我们将等待一半的时间两次:
import time
async def asleep(duration: float):
"""await that ``duration`` seconds pass"""
await AsyncSleep(time.time() + duration / 2)
await AsyncSleep(time.time() + duration / 2)
我们可以直接实例化并运行此协程。类似于生成器,使用coroutine.send
运行协程直到得到yield
结果。
coroutine = asleep(100)
while True:
print(coroutine.send(None))
time.sleep(0.1)
这给了我们两个AsyncSleep
事件,然后是StopIteration
协程完成的一个事件。请注意,唯一的延迟来自time.sleep
循环!每个AsyncSleep
仅存储当前时间的偏移量。
2.2.2。活动+睡眠
目前,我们有两种独立的机制可供使用:
AsyncSleep
可以从协程内部产生的事件
time.sleep
可以等待而不会影响协程
值得注意的是,这两个是正交的:一个都不影响或触发另一个。结果,我们可以提出自己的策略sleep
来应对延迟AsyncSleep
。
2.3。天真的事件循环
如果我们有几个协程,每个协程可以告诉我们何时要唤醒它。然后,我们可以等到第一个恢复之前,然后再恢复,依此类推。值得注意的是,在每一点上我们只关心下一个。
这样可以进行简单的调度:
- 按照所需的唤醒时间对协程进行排序
- 选择第一个想要唤醒的人
- 等到这个时间点
- 运行这个协程
- 从1开始重复。
一个简单的实现不需要任何高级概念。A list
允许按日期对协程进行排序。等待是有规律的time.sleep
。运行协程的工作方式与之前一样coroutine.send
。
def run(*coroutines):
"""Cooperatively run all ``coroutines`` until completion"""
# store wake-up-time and coroutines
waiting = [(0, coroutine) for coroutine in coroutines]
while waiting:
# 2. pick the first coroutine that wants to wake up
until, coroutine = waiting.pop(0)
# 3. wait until this point in time
time.sleep(max(0.0, until - time.time()))
# 4. run this coroutine
try:
command = coroutine.send(None)
except StopIteration:
continue
# 1. sort coroutines by their desired suspension
if isinstance(command, AsyncSleep):
waiting.append((command.until, coroutine))
waiting.sort(key=lambda item: item[0])
当然,这还有很大的改进空间。我们可以将堆用于等待队列,或者将调度表用于事件。我们还可以从中获取返回值,StopIteration
并将其分配给协程。但是,基本原理保持不变。
2.4。合作等待
该AsyncSleep
事件和run
事件循环是定时事件的工作完全实现。
async def sleepy(identifier: str = "coroutine", count=5):
for i in range(count):
print(identifier, 'step', i + 1, 'at %.2f' % time.time())
await asleep(0.1)
run(*(sleepy("coroutine %d" % j) for j in range(5)))
这将在五个协程中的每个协程之间进行协作切换,每个协程暂停0.1秒。即使事件循环是同步的,它仍然可以在0.5秒而不是2.5秒内执行工作。每个协程保持状态并独立运行。
3. I / O事件循环
支持的事件循环sleep
适用于轮询。但是,等待文件句柄上的I / O可以更有效地完成:操作系统实现I / O,因此知道哪些句柄已准备就绪。理想情况下,事件循环应支持显式的“ Ready for I / O”事件。
3.1。该select
呼叫
Python已经有一个接口可以查询OS的读取I / O句柄。当调用带有读取或写入的句柄时,它返回准备读取或写入的句柄:
readable, writeable, _ = select.select(rlist, wlist, xlist, timeout)
例如,我们可以open
写入文件并等待其准备就绪:
write_target = open('/tmp/foo')
readable, writeable, _ = select.select([], [write_target], [])
select返回后,writeable
包含我们的打开文件。
3.2。基本I / O事件
与AsyncSleep
请求类似,我们需要为I / O定义一个事件。使用底层select
逻辑,事件必须引用可读对象-例如open
文件。另外,我们存储要读取的数据量。
class AsyncRead:
def __init__(self, file, amount=1):
self.file = file
self.amount = amount
self._buffer = ''
def __await__(self):
while len(self._buffer) < self.amount:
yield self
# we only get here if ``read`` should not block
self._buffer += self.file.read(1)
return self._buffer
def __repr__(self):
return '%s(file=%s, amount=%d, progress=%d)' % (
self.__class__.__name__, self.file, self.amount, len(self._buffer)
)
与AsyncSleep
我们一样,我们大多只是存储底层系统调用所需的数据。这次__await__
可以恢复多次-直到我们的需求amount
被阅读为止。另外,我们return
的I / O结果不只是恢复。
3.3。使用读取的I / O增强事件循环
事件循环的基础仍然是run
先前定义的。首先,我们需要跟踪读取请求。这不再是排序的时间表,我们仅将读取请求映射到协程。
# new
waiting_read = {} # type: Dict[file, coroutine]
由于select.select
采用了超时参数,因此可以代替time.sleep
。
# old
time.sleep(max(0.0, until - time.time()))
# new
readable, _, _ = select.select(list(reads), [], [])
这将为我们提供所有可读文件-如果有的话,我们将运行相应的协程。如果没有,我们已经等待了足够长的时间来运行当前的协程。
# new - reschedule waiting coroutine, run readable coroutine
if readable:
waiting.append((until, coroutine))
waiting.sort()
coroutine = waiting_read[readable[0]]
最后,我们必须实际侦听读取请求。
# new
if isinstance(command, AsyncSleep):
...
elif isinstance(command, AsyncRead):
...
3.4。把它放在一起
上面有点简化。如果我们总是可以阅读的话,我们需要做一些切换,以免饿死协程。我们需要处理没有阅读或等待的东西。但是,最终结果仍适合30 LOC。
def run(*coroutines):
"""Cooperatively run all ``coroutines`` until completion"""
waiting_read = {} # type: Dict[file, coroutine]
waiting = [(0, coroutine) for coroutine in coroutines]
while waiting or waiting_read:
# 2. wait until the next coroutine may run or read ...
try:
until, coroutine = waiting.pop(0)
except IndexError:
until, coroutine = float('inf'), None
readable, _, _ = select.select(list(waiting_read), [], [])
else:
readable, _, _ = select.select(list(waiting_read), [], [], max(0.0, until - time.time()))
# ... and select the appropriate one
if readable and time.time() < until:
if until and coroutine:
waiting.append((until, coroutine))
waiting.sort()
coroutine = waiting_read.pop(readable[0])
# 3. run this coroutine
try:
command = coroutine.send(None)
except StopIteration:
continue
# 1. sort coroutines by their desired suspension ...
if isinstance(command, AsyncSleep):
waiting.append((command.until, coroutine))
waiting.sort(key=lambda item: item[0])
# ... or register reads
elif isinstance(command, AsyncRead):
waiting_read[command.file] = coroutine
3.5。协同I / O
的AsyncSleep
,AsyncRead
并且run
实现已全功能的睡眠和/或读取。与相同sleepy
,我们可以定义一个帮助程序来测试阅读:
async def ready(path, amount=1024*32):
print('read', path, 'at', '%d' % time.time())
with open(path, 'rb') as file:
result = return await AsyncRead(file, amount)
print('done', path, 'at', '%d' % time.time())
print('got', len(result), 'B')
run(sleepy('background', 5), ready('/dev/urandom'))
运行此命令,我们可以看到我们的I / O与等待的任务交错:
id background round 1
read /dev/urandom at 1530721148
id background round 2
id background round 3
id background round 4
id background round 5
done /dev/urandom at 1530721148
got 1024 B
4.非阻塞I / O
虽然文件上的I / O可以理解这个概念,但它实际上并不适合于像这样的库asyncio
:select
调用总是返回文件,并且两者都调用,open
并且read
可能无限期地阻塞。这阻止了事件循环的所有协程-这很糟糕。诸如此类的库aiofiles
使用线程和同步来伪造文件中的非阻塞I / O和事件。
但是,套接字确实允许无阻塞的I / O-并且它们固有的延迟使其变得更加关键。在事件循环中使用时,可以包装等待数据和重试而不会阻塞任何内容。
4.1。非阻塞I / O事件
与我们类似AsyncRead
,我们可以为套接字定义一个暂停和读取事件。我们不使用文件,而是使用套接字-该套接字必须是非阻塞的。另外,我们__await__
使用socket.recv
代替file.read
。
class AsyncRecv:
def __init__(self, connection, amount=1, read_buffer=1024):
assert not connection.getblocking(), 'connection must be non-blocking for async recv'
self.connection = connection
self.amount = amount
self.read_buffer = read_buffer
self._buffer = b''
def __await__(self):
while len(self._buffer) < self.amount:
try:
self._buffer += self.connection.recv(self.read_buffer)
except BlockingIOError:
yield self
return self._buffer
def __repr__(self):
return '%s(file=%s, amount=%d, progress=%d)' % (
self.__class__.__name__, self.connection, self.amount, len(self._buffer)
)
与相比AsyncRead
,__await__
执行真正的非阻塞I / O。当有数据时,它总是读取。如果没有可用数据,它将始终挂起。这意味着仅在我们执行有用的工作时才阻止事件循环。
4.2。解除阻塞事件循环
就事件循环而言,没有什么变化。要监听的事件仍然与文件相同-由标记为ready的文件描述符select
。
# old
elif isinstance(command, AsyncRead):
waiting_read[command.file] = coroutine
# new
elif isinstance(command, AsyncRead):
waiting_read[command.file] = coroutine
elif isinstance(command, AsyncRecv):
waiting_read[command.connection] = coroutine
在这一点上,显然与AsyncRead
和AsyncRecv
是同一种事件。我们可以轻松地将它们重构为一个具有可交换I / O组件的事件。实际上,事件循环,协程和事件将调度程序,任意中间代码和实际I / O 清晰地分开。
4.3。非阻塞I / O的丑陋一面
原则上,你应该在这一点上做的是复制的逻辑read
作为recv
对AsyncRecv
。但是,这现在变得更加丑陋-当函数在内核内部阻塞时,您必须处理早期返回,但要对您产生控制权。例如,打开连接与打开文件的时间更长:
# file
file = open(path, 'rb')
# non-blocking socket
connection = socket.socket()
connection.setblocking(False)
# open without blocking - retry on failure
try:
connection.connect((url, port))
except BlockingIOError:
pass
长话短说,剩下的就是几十行异常处理。此时事件和事件循环已经起作用。
id background round 1
read localhost:25000 at 1530783569
read /dev/urandom at 1530783569
done localhost:25000 at 1530783569 got 32768 B
id background round 2
id background round 3
id background round 4
done /dev/urandom at 1530783569 got 4096 B
id background round 5
附录
github上的示例代码
BaseEventLoop
是如何实现的:github.com/python/cpython/blob/…–