最近通过的PEP-0492为 Python 3.5 在处理协程时增加了一些特殊的语法。新功能中很大一部分在3.5 之前的版本就已经有了,不过之前的语法并不算最好的,因为生成器和协程的概念本身就有点容易混淆。PEP-0492 通过使用 async 关键字显式的对生成器和协程做了区分。

本文旨在说明这些新的机制在底层是如何工作的。如果你只是对怎么使用这些功能感兴趣,那我建议你可以忽略这篇文章,而是去看一下内置的 asyncio 模块的文档。如果你对底层的概念感兴趣,关心这些底层功能如何能构建你自己的 asyncio 模块,那你会发现本文会有有意思。

本文中我们会完全放弃任何异步 I/O 方法,而只限于使用多协程的交互。下面是两个很小的函数:

def coro1():
    print("C1: Start")
    print("C1: Stop")
 
def coro2():
    print("C2: Start")
    print("C2: Stop")

我们从两个最简单的函数开始,coro1和coro2。我们可以按顺序来执行这两个函数:

coro1()
coro2()

我们得到期望的输出结果:

C1: Start
C1: Stop
C2: Start
C2: Stop

不过,基于某些原因,我们可能会期望这些代码交互运行。普通的函数做不到这点,所以我们把这些函数转换成携程:

async def coro1():
    print("C1: Start")
    print("C1: Stop")
 
async def coro2():
    print("C2: Start")
    print("C2: Stop")

通过新的 async 关键字的魔法,这些函数不再是函数了,现在它们变成了协程(更准确的说是本地协程函数)。普通函数被调用的时候,函数体会被执行,但是在调用协程函数的时候,函数体并不会被执行,你得到的是一个协程对象:

c1 = coro1()
c2 = coro2()
print(c1, c2)

输出:

<coroutine object coro1 at 0x10ea60990> <coroutine object coro2 at 0x10ea60a40>

(解释器还会打印一些运行时的警告信息,先忽略掉)。

那么,为什么要有一个协程对象?代码到底如何执行?执行协程的一种方式是使用 await 表达式(使用新的 await 关键字)。你可能会想,可以这样来做:

关键之处是协程确实是与 Python 的生成器非常相似,也都有一个 send 方法。我们可以通过调用 send 方法来启动一个协程的执行。

c1.send(None)

这样我们的第一个协程终于可以执行完成了,不过我们也得到了一个讨厌的 StopIteration 异常:

C1: Start
C1: Stop
Traceback (most recent call last):
  File "test3.py", line 16, in
    c1.send(None)
StopIteration

StopIteration 异常是一种标记生成器(或者像这里的协程)执行结束的机制。虽然这是一个异常,但是确实是我们期望的!我们可以用适当的 try-catch 代码将其包起来,这样就可以避免错误提示。接下来我们让我们的第二个协程也执行起来:

try:
    c1.send(None)
except StopIteration:
    pass
try:
    c2.send(None)
except StopIteration:
    pass

我们希望的是有一个函数来控制所有的协程的运行,直到全部协程都执行完成。换句话说,我们期望连续不断的调用 send,驱动不同的协程去执行,直到send抛出 StopIteration 异常。

为此我们新建一个函数,这个函数传入一个协程列表,函数执行这些协程直到全部结束。我们现在要做的就是调用这个函数。

def run(coros):
    coros = list(coros)
 
    while coros:
        # Duplicate list for iteration so we can remove from original list.
        for coro in list(coros):
            try:
                coro.send(None)
            except StopIteration:
                coros.remove(coro)

这段代码每次从协程列表里取一个协程执行,如果捕获到 StopIteration 异常,就把这个协程从队列里去掉。

接下来我们把手工调用 send 的代码去掉,代码如下:

c1 = coro1()
c2 = coro2()
run([c1, c2])

 

 

import random
import asyncio

async def asyn(name='0'):
    print("{} begin".format(name))
    r = await asyncio.sleep(random.randint(1, 3))
    print("{} end".format(name))
    return "{} finish".format(name)


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    tasks = [asyncio.ensure_future(asyn(str(i))) for i in range(3)]
    loop.run_until_complete(asyncio.wait(tasks))
    print("all task is finish")
    loop.close()