python 协程

以下为从各网站抄录的相关文章,包括jobbole(伯乐在线,1,2),segmentfault(思否,3)
前两篇太难,初级阶段看3以及3的前篇就行
1: http://python.jobbole.com/86481/
2: http://python.jobbole.com/87310/
3: https://segmentfault.com/a/1190000009781688

---------------------------------------------------文章1:----------------------------------------------------

Python 3.5 协程究竟是什么

原文出处: snarky 译文出处:Yushneng
作为 Python 核心开发者之一,让我很想了解这门语言是如何运作的。我发现总有一些阴暗的角落我对其中错综复杂的细节不是很清楚,但是为了能够有助于 Python 的一些问题和其整体设计,我觉得我应该试着去理解 Python 的核心语法和内部运作机制。

但是直到最近我才理解 Python 3.5 中 async/await 的原理。我知道 Python 3.3 中的 yield from 和 Python 3.4 中的 asyncio 组合得来这一新语法。但较少处理网络相关的问题 – asyncio 并不仅限于此但确是重要用途 – 使我没太注意 async/await 。我知道:

yield from iterator
1
yield from iterator
(本质上)相当于:

for x in iterator:
yield x
1
2
for x in iterator:
yield x
我知道 asyncio 是事件循环框架可以进行异步编程,但是我只是知道这里面每个单词的意思而已,从没深入研究 async/await 语法组合背后的原理,我发现不理解 Python 中的异步编程已经对我造成了困扰。因此我决定花时间弄清楚这背后的原理究竟是什么。我从很多人那里得知他们也不了解异步编程的原理,因此我决定写这篇论文(是的,由于这篇文章花费时间之久以及篇幅之长,我的妻子已经将其定义为一篇论文)。

由于我想要正确地理解这些语法的原理,这篇文章涉及到一些关于 CPython 较为底层的技术细节。如果这些细节超出了你想了解的内容,或者你不能完全理解它们,都没关系,因为我为了避免这篇文章演变成一本书那么长,省略了一些 CPython 内部的细枝末节(比如说,如果你不知道 code object 有 flags,甚至不知道什么是 code object,这都没关系,也不用一定要从这篇文字中获得什么)。我试着在最后一小节中用更直接的方法做了总结,如果觉得文章对你来说细节太多,你完全可以跳过。

关于 Python 协程的历史课
根据维基百科给出的定义,“协程 是为非抢占式多任务产生子程序的计算机程序组件,协程允许不同入口点在不同位置暂停或开始执行程序”。从技术的角度来说,“协程就是你可以暂停执行的函数”。如果你把它理解成“就像生成器一样”,那么你就想对了。

退回到 Python 2.2,生成器第一次在PEP 255中提出(那时也把它成为迭代器,因为它实现了迭代器协议)。主要是受到Icon编程语言的启发,生成器允许创建一个在计算下一个值时不会浪费内存空间的迭代器。例如你想要自己实现一个 range() 函数,你可以用立即计算的方式创建一个整数列表:

Python

def eager_range(up_to):
    """Create a list of integers, from 0 to up_to, exclusive."""
    sequence = []
    index = 0
    while index < up_to:
        sequence.append(index)
        index += 1
    return sequence

1
2
3
4
5
6
7
8

def eager_range(up_to):
    """Create a list of integers, from 0 to up_to, exclusive."""
    sequence = []
    index = 0
    while index < up_to:
        sequence.append(index)
        index += 1
    return sequence

然而这里存在的问题是,如果你想创建从0到1,000,000这样一个很大的序列,你不得不创建能容纳1,000,000个整数的列表。但是当加入了生成器之后,你可以不用创建完整的序列,你只需要能够每次保存一个整数的内存即可。

Python

def lazy_range(up_to):
    """Generator to return the sequence of integers from 0 to up_to, exclusive."""
    index = 0
    while index < up_to:
        yield index
        index += 1

1
2
3
4
5
6

def lazy_range(up_to):
    """Generator to return the sequence of integers from 0 to up_to, exclusive."""
    index = 0
    while index < up_to:
        yield index
        index += 1

让函数遇到 yield 表达式时暂停执行 – 虽然在 Python 2.5 以前它只是一条语句 – 并且能够在后面重新执行,这对于减少内存使用、生成无限序列非常有用。

你有可能已经发现,生成器完全就是关于迭代器的。有一种更好的方式生成迭代器当然很好(尤其是当你可以给一个生成器对象添加 iter() 方法时),但是人们知道,如果可以利用生成器“暂停”的部分,添加“将东西发送回生成器”的功能,那么 Python 突然就有了协程的概念(当然这里的协程仅限于 Python 中的概念;Python 中真实的协程在后面才会讨论)。将东西发送回暂停了的生成器这一特性通过 PEP 342添加到了 Python 2.5。与其它特性一起,PEP 342 为生成器引入了 send() 方法。这让我们不仅可以暂停生成器,而且能够传递值到生成器暂停的地方。还是以我们的 range() 为例,你可以让序列向前或向后跳过几个值:

Python

def jumping_range(up_to):
    """Generator for the sequence of integers from 0 to up_to, exclusive.

    Sending a value into the generator will shift the sequence by that amount.
    """
    index = 0
    while index < up_to:
        jump = yield index
        if jump is None:
            jump = 1
        index += jump

if __name__ == '__main__':
    iterator = jumping_range(5)
    print(next(iterator))  # 0
    print(iterator.send(2))  # 2
    print(next(iterator))  # 3
    print(iterator.send(-1))  # 2
    for x in iterator:
        print(x)  # 3, 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

def jumping_range(up_to):
    """Generator for the sequence of integers from 0 to up_to, exclusive.
 
    Sending a value into the generator will shift the sequence by that amount.
    """
    index = 0
    while index < up_to:
        jump = yield index
        if jump is None:
            jump = 1
        index += jump
 
if __name__ == '__main__':
    iterator = jumping_range(5)
    print(next(iterator))  # 0
    print(iterator.send(2))  # 2
    print(next(iterator))  # 3
    print(iterator.send(-1))  # 2
    for x in iterator:
        print(x)  # 3, 4

直到PEP 380 为 Python 3.3 添加了 yield from之前,生成器都没有变动。严格来说,这一特性让你能够从迭代器(生成器刚好也是迭代器)中返回任何值,从而可以干净利索的方式重构生成器。

Python

def lazy_range(up_to):
    """Generator to return the sequence of integers from 0 to up_to, exclusive."""
    index = 0
    def gratuitous_refactor():
        while index < up_to:
            yield index
            index += 1
    yield from gratuitous_refactor()

1
2
3
4
5
6
7
8

def lazy_range(up_to):
    """Generator to return the sequence of integers from 0 to up_to, exclusive."""
    index = 0
    def gratuitous_refactor():
        while index < up_to:
            yield index
            index += 1
    yield from gratuitous_refactor()

yield from 通过让重构变得简单,也让你能够将生成器串联起来,使返回值可以在调用栈中上下浮动,而不需对编码进行过多改动。

def bottom():
    # Returning the yield lets the value that goes up the call stack to come right back
    # down.
    return (yield 42)

def middle():
    return (yield from bottom())

def top():
    return (yield from middle())

Get the generator.

gen = top()
value = next(gen)
print(value)  # Prints '42'.
try:
    value = gen.send(value * 2)
except StopIteration as exc:
    value = exc.value
print(value)  # Prints '84'.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

def bottom():
    # Returning the yield lets the value that goes up the call stack to come right back
    # down.
    return (yield 42)
 
def middle():
    return (yield from bottom())
 
def top():
    return (yield from middle())

Get the generator.

gen = top()
value = next(gen)
print(value)  # Prints '42'.
try:
    value = gen.send(value * 2)
except StopIteration as exc:
    value = exc.value
print(value)  # Prints '84'.

总结
Python 2.2 中的生成器让代码执行过程可以暂停。Python 2.5 中可以将值返回给暂停的生成器,这使得 Python 中协程的概念成为可能。加上 Python 3.3 中的 yield from,使得重构生成器与将它们串联起来都很简单。

什么是事件循环?
如果你想了解 async/await,那么理解什么是事件循环以及它是如何让异步编程变为可能就相当重要了。如果你曾做过 GUI 编程 – 包括网页前端工作 – 那么你已经和事件循环打过交道。但是由于异步编程的概念作为 Python 语言结构的一部分还是最近才有的事,你刚好不知道什么是事件循环也很正常。

回到维基百科,事件循环 “是一种等待程序分配事件或消息的编程架构”。基本上来说事件循环就是,“当A发生时,执行B”。或许最简单的例子来解释这一概念就是用每个浏览器中都存在的JavaScript事件循环。当你点击了某个东西(“当A发生时”),这一点击动作会发送给JavaScript的事件循环,并检查是否存在注册过的 onclick 回调来处理这一点击(“执行B”)。只要有注册过的回调函数就会伴随点击动作的细节信息被执行。事件循环被认为是一种循环是因为它不停地收集事件并通过循环来发如何应对这些事件。

对 Python 来说,用来提供事件循环的 asyncio 被加入标准库中。asyncio 重点解决网络服务中的问题,事件循环在这里将来自套接字(socket)的 I/O 已经准备好读和/或写作为“当A发生时”(通过selectors模块)。除了 GUI 和 I/O,事件循环也经常用于在别的线程或子进程中执行代码,并将事件循环作为调节机制(例如,合作式多任务)。如果你恰好理解 Python 的 GIL,事件循环对于需要释放 GIL 的地方很有用。

总结
事件循环提供一种循环机制,让你可以“在A发生时,执行B”。基本上来说事件循环就是监听当有什么发生时,同时事件循环也关心这件事并执行相应的代码。Python 3.4 以后通过标准库 asyncio 获得了事件循环的特性。

async 和 await 是如何运作的
Python 3.4 中的方式
在 Python 3.3 中出现的生成器与之后以 asyncio 的形式出现的事件循环之间,Python 3.4 通过并发编程的形式已经对异步编程有了足够的支持。异步编程简单来说就是代码执行的顺序在程序运行前是未知的(因此才称为异步而非同步)。并发编程是代码的执行不依赖于其他部分,即便是全都在同一个线程内执行(并发不是并行)。例如,下面 Python 3.4 的代码分别以异步和并发的函数调用实现按秒倒计时。

import asyncio

#Borrowed from http://curio.readthedocs.org/en/latest/tutorial.html.
@asyncio.coroutine
def countdown(number, n):
    while n > 0:
        print('T-minus', n, '({})'.format(number))
        yield from asyncio.sleep(1)
        n -= 1

loop = asyncio.get_event_loop()
tasks = [
    asyncio.ensure_future(countdown("A", 2)),
    asyncio.ensure_future(countdown("B", 3))]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import asyncio

Borrowed from http://curio.readthedocs.org/en/latest/tutorial.html.

@asyncio.coroutine
def countdown(number, n):
    while n > 0:
        print('T-minus', n, '({})'.format(number))
        yield from asyncio.sleep(1)
        n -= 1
 
loop = asyncio.get_event_loop()
tasks = [
    asyncio.ensure_future(countdown("A", 2)),
    asyncio.ensure_future(countdown("B", 3))]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

Python 3.4 中,asyncio.coroutine 修饰器用来标记作为协程的函数,这里的协程是和asyncio及其事件循环一起使用的。这赋予了 Python 第一个对于协程的明确定义:实现了PEP 342添加到生成器中的这一方法的对象,并通过[collections.abc.Coroutine这一抽象基类]表征的对象。这意味着突然之间所有实现了协程接口的生成器,即便它们并不是要以协程方式应用,都符合这一定义。为了修正这一点,asyncio 要求所有要用作协程的生成器必须由asyncio.coroutine修饰。

有了对协程明确的定义(能够匹配生成器所提供的API),你可以对任何asyncio.Future对象使用 yield from,从而将其传递给事件循环,暂停协程的执行来等待某些事情的发生( future 对象并不重要,只是asyncio细节的实现)。一旦 future 对象获取了事件循环,它会一直在那里监听,直到完成它需要做的一切。当 future 完成自己的任务之后,事件循环会察觉到,暂停并等待在那里的协程会通过send()方法获取future对象的返回值并开始继续执行。

以上面的代码为例。事件循环启动每一个 countdown() 协程,一直执行到遇见其中一个协程的 yield from 和 asyncio.sleep() 。这样会返回一个 asyncio.Future对象并将其传递给事件循环,同时暂停这一协程的执行。事件循环会监控这一future对象,直到倒计时1秒钟之后(同时也会检查其它正在监控的对象,比如像其它协程)。1秒钟的时间一到,事件循环会选择刚刚传递了future对象并暂停了的 countdown() 协程,将future对象的结果返回给协程,然后协程可以继续执行。这一过程会一直持续到所有的 countdown() 协程执行完毕,事件循环也被清空。稍后我会给你展示一个完整的例子,用来说明协程/事件循环之类的这些东西究竟是如何运作的,但是首先我想要解释一下async和await。

Python 3.5 从 yield from 到 await
在 Python 3.4 中,用于异步编程并被标记为协程的函数看起来是这样的:

This also works in Python 3.5.

@asyncio.coroutine
def py34_coro():
    yield from stuff()

1
2
3
4

This also works in Python 3.5.

@asyncio.coroutine
def py34_coro():
    yield from stuff()

Python 3.5 添加了types.coroutine 修饰器,也可以像 asyncio.coroutine 一样将生成器标记为协程。你可以用 async def 来定义一个协程函数,虽然这个函数不能包含任何形式的 yield 语句;只有 return 和 await 可以从协程中返回值。

async def py35_coro():
    await stuff()

1
2

async def py35_coro():
    await stuff()

虽然 async 和 types.coroutine 的关键作用在于巩固了协程的定义,但是它将协程从一个简单的接口变成了一个实际的类型,也使得一个普通生成器和用作协程的生成器之间的差别变得更加明确(inspect.iscoroutine() 函数 甚至明确规定必须使用 async 的方式定义才行)。

你将发现不仅仅是 async,Python 3.5 还引入 await 表达式(只能用于async def中)。虽然await的使用和yield from很像,但await可以接受的对象却是不同的。await 当然可以接受协程,因为协程的概念是所有这一切的基础。但是当你使用 await 时,其接受的对象必须是awaitable 对象:必须是定义了__await__()方法且这一方法必须返回一个不是协程的迭代器。协程本身也被认为是 awaitable 对象(这也是collections.abc.Coroutine 继承 collections.abc.Awaitable的原因)。这一定义遵循 Python 将大部分语法结构在底层转化成方法调用的传统,就像 a + b 实际上是a.add(b) 或者 b.radd(a)。

yield from 和 await 在底层的差别是什么(也就是types.coroutine与async def的差别)?让我们看一下上面两则Python 3.5代码的例子所产生的字节码在本质上有何差异。py34_coro()的字节码是:

dis.dis(py34_coro)
2 0 LOAD_GLOBAL 0 (stuff)
3 CALL_FUNCTION 0 (0 positional, 0 keyword pair)
6 GET_YIELD_FROM_ITER
7 LOAD_CONST 0 (None)
10 YIELD_FROM
11 POP_TOP
12 LOAD_CONST 0 (None)
15 RETURN_VALUE
1
2
3
4
5
6
7
8
9

dis.dis(py34_coro)
2 0 LOAD_GLOBAL 0 (stuff)
3 CALL_FUNCTION 0 (0 positional, 0 keyword pair)
6 GET_YIELD_FROM_ITER
7 LOAD_CONST 0 (None)
10 YIELD_FROM
11 POP_TOP
12 LOAD_CONST 0 (None)
15 RETURN_VALUE
py35_coro()的字节码是:

dis.dis(py35_coro)
1 0 LOAD_GLOBAL 0 (stuff)
3 CALL_FUNCTION 0 (0 positional, 0 keyword pair)
6 GET_AWAITABLE
7 LOAD_CONST 0 (None)
10 YIELD_FROM
11 POP_TOP
12 LOAD_CONST 0 (None)
15 RETURN_VALUE
1
2
3
4
5
6
7
8
9

dis.dis(py35_coro)
1 0 LOAD_GLOBAL 0 (stuff)
3 CALL_FUNCTION 0 (0 positional, 0 keyword pair)
6 GET_AWAITABLE
7 LOAD_CONST 0 (None)
10 YIELD_FROM
11 POP_TOP
12 LOAD_CONST 0 (None)
15 RETURN_VALUE
忽略由于py34_coro()的asyncio.coroutine 修饰器所带来的行号的差别,两者之间唯一可见的差异是GET_YIELD_FROM_ITER操作码 对比GET_AWAITABLE操作码。两个函数都被标记为协程,因此在这里没有差别。GET_YIELD_FROM_ITER 只是检查参数是生成器还是协程,否则将对其参数调用iter()方法(只有用在协程内部的时候yield from所对应的操作码才可以接受协程对象,在这个例子里要感谢types.coroutine修饰符将这个生成器在C语言层面标记为CO_ITERABLE_COROUTINE)。

但是 GET_AWAITABLE的做法不同,其字节码像GET_YIELD_FROM_ITER一样接受协程,但是不接受没有被标记为协程的生成器。就像前面讨论过的一样,除了协程以外,这一字节码还可以接受awaitable对象。这使得yield from和await表达式都接受协程但分别接受一般的生成器和awaitable对象。

你可能会想,为什么基于async的协程和基于生成器的协程会在对应的暂停表达式上面有所不同?主要原因是出于最优化Python性能的考虑,确保你不会将刚好有同样API的不同对象混为一谈。由于生成器默认实现协程的API,因此很有可能在你希望用协程的时候错用了一个生成器。而由于并不是所有的生成器都可以用在基于协程的控制流中,你需要避免错误地使用生成器。但是由于 Python 并不是静态编译的,它最好也只能在用基于生成器定义的协程时提供运行时检查。这意味着当用types.coroutine时,Python 的编译器将无法判断这个生成器是用作协程还是仅仅是普通的生成器(记住,仅仅因为types.coroutine这一语法的字面意思,并不意味着在此之前没有人做过types = spam的操作),因此编译器只能基于当前的情况生成有着不同限制的操作码。

关于基于生成器的协程和async定义的协程之间的差异,我想说明的关键点是只有基于生成器的协程可以真正的暂停执行并强制性返回给事件循环。你可能不了解这些重要的细节,因为通常你调用的像是asyncio.sleep() function 这种事件循环相关的函数,由于事件循环实现他们自己的API,而这些函数会处理这些小的细节。对于我们绝大多数人来说,我们只会跟事件循环打交道,而不需要处理这些细节,因此可以只用async定义的协程。但是如果你和我一样好奇为什么不能在async定义的协程中使用asyncio.sleep(),那么这里的解释应该可以让你顿悟。

总结
让我们用简单的话来总结一下。用async def可以定义得到协程。定义协程的另一种方式是通过types.coroutine修饰器 — 从技术实现的角度来说就是添加了 CO_ITERABLE_COROUTINE标记 — 或者是collections.abc.Coroutine的子类。你只能通过基于生成器的定义来实现协程的暂停。

awaitable 对象要么是一个协程要么是一个定义了__await__()方法的对象 — 也就是collections.abc.Awaitable — 且__await__()必须返回一个不是协程的迭代器。await表达式基本上与yield from相同但只能接受awaitable对象(普通迭代器不行)。async定义的函数要么包含return语句 — 包括所有Python函数缺省的return None — 和/或者 await表达式(yield表达式不行)。async函数的限制确保你不会将基于生成器的协程与普通的生成器混合使用,因为对这两种生成器的期望是非常不同的。

将 async/await 看做异步编程的 API
我想要重点指出的地方实际上在我看David Beazley’s Python Brasil 2015 keynote之前还没有深入思考过。在他的演讲中,David 指出 async/await 实际上是异步编程的 API (他在 Twitter 上向我重申过)。David 的意思是人们不应该将async/await等同于asyncio,而应该将asyncio看作是一个利用async/await API 进行异步编程的框架。

David 将 async/await 看作是异步编程的API创建了 curio 项目来实现他自己的事件循环。这帮助我弄清楚 async/await 是 Python 创建异步编程的原料,同时又不会将你束缚在特定的事件循环中也无需与底层的细节打交道(不像其他编程语言将事件循环直接整合到语言中)。这允许像 curio 一样的项目不仅可以在较低层面上拥有不同的操作方式(例如 asyncio 利用 future 对象作为与事件循环交流的 API,而 curio 用的是元组),同时也可以集中解决不同的问题,实现不同的性能特性(例如 asyncio 拥有一整套框架来实现运输层和协议层,从而使其变得可扩展,而 curio 只是简单地让用户来考虑这些但同时也让它运行地更快)。

考虑到 Python 异步编程的(短暂)历史,可以理解人们会误认为 async/await == asyncio。我是说asyncio帮助我们可以在 Python 3.4 中实现异步编程,同时也是 Python 3.5 中引入async/await的推动因素。但是async/await 的设计意图就是为了让其足够灵活从而不需要依赖asyncio或者仅仅是为了适应这一框架而扭曲关键的设计决策。换句话说,async/await 延续了 Python 设计尽可能灵活的传统同时又非常易于使用(实现)。

一个例子
到这里你的大脑可能已经灌满了新的术语和概念,导致你想要从整体上把握所有这些东西是如何让你可以实现异步编程的稍微有些困难。为了帮助你让这一切更加具体化,这里有一个完整的(伪造的)异步编程的例子,将代码与事件循环及其相关的函数一一对应起来。这个例子里包含的几个协程,代表着火箭发射的倒计时,并且看起来是同时开始的。这是通过并发实现的异步编程;3个不同的协程将分别独立运行,并且都在同一个线程内完成。

Python

import datetime
import heapq
import types
import time

class Task:

"""Represent how long a coroutine should before starting again.

Comparison operators are implemented for use by heapq. Two-item
tuples unfortunately don't work because when the datetime.datetime
instances are equal, comparison falls to the coroutine and they don't
implement comparison methods, triggering an exception.

Think of this as being like asyncio.Task/curio.Task.
"""

def __init__(self, wait_until, coro):
    self.coro = coro
    self.waiting_until = wait_until

def __eq__(self, other):
    return self.waiting_until == other.waiting_until

def __lt__(self, other):
    return self.waiting_until < other.waiting_until

class SleepingLoop:

"""An event loop focused on delaying execution of coroutines.

Think of this as being like asyncio.BaseEventLoop/curio.Kernel.
"""

def __init__(self, *coros):
    self._new = coros
    self._waiting = []

def run_until_complete(self):
    # Start all the coroutines.
    for coro in self._new:
        wait_for = coro.send(None)
        heapq.heappush(self._waiting, Task(wait_for, coro))
    # Keep running until there is no more work to do.
    while self._waiting:
        now = datetime.datetime.now()
        # Get the coroutine with the soonest resumption time.
        task = heapq.heappop(self._waiting)
        if now < task.waiting_until:
            # We're ahead of schedule; wait until it's time to resume.
            delta = task.waiting_until - now
            time.sleep(delta.total_seconds())
            now = datetime.datetime.now()
        try:
            # It's time to resume the coroutine.
            wait_until = task.coro.send(now)
            heapq.heappush(self._waiting, Task(wait_until, task.coro))
        except StopIteration:
            # The coroutine is done.
            pass

@types.coroutine
def sleep(seconds):
    """Pause a coroutine for the specified number of seconds.
Think of this as being like asyncio.sleep()/curio.sleep().
"""
now = datetime.datetime.now()
wait_until = now + datetime.timedelta(seconds=seconds)
# Make all coroutines on the call stack pause; the need to use `yield`
# necessitates this be generator-based and not an async-based coroutine.
actual = yield wait_until
# Resume the execution stack, sending back how long we actually waited.
return actual - now

async def countdown(label, length, *, delay=0):
    """Countdown a launch for `length` seconds, waiting `delay` seconds.
This is what a user would typically write.
"""
print(label, 'waiting', delay, 'seconds before starting countdown')
delta = await sleep(delay)
print(label, 'starting after waiting', delta)
while length:
    print(label, 'T-minus', length)
    waited = await sleep(1)
    length -= 1
print(label, 'lift-off!')

def main():
    """Start the event loop, counting down 3 separate launches.
This is what a user would typically write.
"""
loop = SleepingLoop(countdown('A', 5), countdown('B', 3, delay=2),
                    countdown('C', 4, delay=1))
start = datetime.datetime.now()
loop.run_until_complete()
print('Total elapsed time is', datetime.datetime.now() - start)

if __name__ == '__main__':
    main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102

import datetime
import heapq
import types
import time
 
class Task:
 
    """Represent how long a coroutine should before starting again.
 
    Comparison operators are implemented for use by heapq. Two-item
    tuples unfortunately don't work because when the datetime.datetime
    instances are equal, comparison falls to the coroutine and they don't
    implement comparison methods, triggering an exception.


Think of this as being like asyncio.Task/curio.Task.
"""

def __init__(self, wait_until, coro):
    self.coro = coro
    self.waiting_until = wait_until

def __eq__(self, other):
    return self.waiting_until == other.waiting_until

def __lt__(self, other):
    return self.waiting_until < other.waiting_until


class SleepingLoop:
 
"""An event loop focused on delaying execution of coroutines.

Think of this as being like asyncio.BaseEventLoop/curio.Kernel.
"""

def __init__(self, *coros):
    self._new = coros
    self._waiting = []

def run_until_complete(self):
    # Start all the coroutines.
    for coro in self._new:
        wait_for = coro.send(None)
        heapq.heappush(self._waiting, Task(wait_for, coro))
    # Keep running until there is no more work to do.
    while self._waiting:
        now = datetime.datetime.now()
        # Get the coroutine with the soonest resumption time.
        task = heapq.heappop(self._waiting)
        if now < task.waiting_until:
            # We're ahead of schedule; wait until it's time to resume.
            delta = task.waiting_until - now
            time.sleep(delta.total_seconds())
            now = datetime.datetime.now()
        try:
            # It's time to resume the coroutine.
            wait_until = task.coro.send(now)
            heapq.heappush(self._waiting, Task(wait_until, task.coro))
        except StopIteration:
            # The coroutine is done.
            pass


@types.coroutine
def sleep(seconds):
    """Pause a coroutine for the specified number of seconds.


Think of this as being like asyncio.sleep()/curio.sleep().
"""
now = datetime.datetime.now()
wait_until = now + datetime.timedelta(seconds=seconds)
# Make all coroutines on the call stack pause; the need to use `yield`
# necessitates this be generator-based and not an async-based coroutine.
actual = yield wait_until
# Resume the execution stack, sending back how long we actually waited.
return actual - now


async def countdown(label, length, *, delay=0):
    """Countdown a launch for `length` seconds, waiting `delay` seconds.
 
This is what a user would typically write.
"""
print(label, 'waiting', delay, 'seconds before starting countdown')
delta = await sleep(delay)
print(label, 'starting after waiting', delta)
while length:
    print(label, 'T-minus', length)
    waited = await sleep(1)
    length -= 1
print(label, 'lift-off!')


def main():
    """Start the event loop, counting down 3 separate launches.
 
This is what a user would typically write.
"""
loop = SleepingLoop(countdown('A', 5), countdown('B', 3, delay=2),
                    countdown('C', 4, delay=1))
start = datetime.datetime.now()
loop.run_until_complete()
print('Total elapsed time is', datetime.datetime.now() - start)


if __name__ == '__main__':
    main()

就像我说的,这是伪造出来的,但是如果你用 Python 3.5 去运行,你会发现这三个协程在同一个线程内独立运行,并且总的运行时间大约是5秒钟。你可以将Task,SleepingLoop和sleep()看作是事件循环的提供者,就像asyncio和curio所提供给你的一样。对于一般的用户来说,只有countdown()和main()函数中的代码才是重要的。正如你所见,async和await或者是这整个异步编程的过程并没什么黑科技;只不过是 Python 提供给你帮助你更简单地实现这类事情的API。

我对未来的希望和梦想
现在我理解了 Python 中的异步编程是如何运作的了,我想要一直用它!这是如此绝妙的概念,比你之前用过的线程好太多了。但是问题在于 Python 3.5 还太新了,async/await也太新了。这意味着还没有太多库支持这样的异步编程。例如,为了实现 HTTP 请求你要么不得不自己徒手构建 ,要么用像是 aiohttp 之类的框架 将 HTTP 添加在另外一个事件循环的顶端,或者寄希望于更多像hyper 库一样的项目不停涌现,可以提供对于 HTTP 之类的抽象,可以让你随便用任何 I/O 库 来实现你的需求(虽然可惜的是 hyper目前只支持 HTTP/2)。

对于我个人来说,我希望更多像hyper一样的项目可以脱颖而出,这样我们就可以在从 I/O中读取与解读二进制数据之间做出明确区分。这样的抽象非常重要,因为Python多数 I/O 库中处理 I/O 和处理数据是紧紧耦合在一起的。Python 的标准库 http就有这样的问题,它不提供 HTTP解析而只有一个连接对象为你处理所有的 I/O。而如果你寄希望于requests可以支持异步编程,那你的希望已经破灭了,因为 requests 的同步 I/O 已经烙进它的设计中了。Python 在网络堆栈上很多层都缺少抽象定义,异步编程能力的改进使得 Python 社区有机会对此作出修复。我们可以很方便地让异步代码像同步一样执行,这样一些填补异步编程空白的工具可以安全地运行在两种环境中。

我希望 Python 可以让 async 协程支持 yield。或者需要用一个新的关键词来实现(可能像 anticipate之类?),因为不能仅靠async就实现事件循环让我很困扰。幸运的是,我不是唯一一个这么想的人,而且PEP 492的作者也和我意见一致,我觉得还是有机会可以移除掉这点小瑕疵。

结论
基本上 async 和 await 产生神奇的生成器,我们称之为协程,同时需要一些额外的支持例如 awaitable 对象以及将普通生成器转化为协程。所有这些加到一起来支持并发,这样才使得 Python 更好地支持异步编程。相比类似功能的线程,这是一个更妙也更简单的方法。我写了一个完整的异步编程例子,算上注释只用了不到100行 Python 代码 — 但仍然非常灵活与快速(curio FAQ 指出它比 twisted 要快 30-40%,但是要比 gevent 慢 10-15%,而且全部都是有纯粹的 Python 实现的;记住Python 2 + Twisted 内存消耗更少同时比Go更容易调试,想象一下这些能帮你实现什么吧!)。我非常高兴这些能够在 Python 3 中成为现实,我也非常期待 Python 社区可以接纳并将其推广到各种库和框架中区,可以使我们都能够受益于 Python 异步编程带来的好处!

------------------------------------------------文章2:-----------------------------------------------------

Python黑魔法 — 异步IO( asyncio) 协程

2017/02/03 · 基础知识 · 3 评论 · asyncio, 协程

原文出处: 人间世

python asyncio

网络模型有很多中,为了实现高并发也有很多方案,多线程,多进程。无论多线程和多进程,IO的调度更多取决于系统,而协程的方式,调度来自用户,用户可以在函数中yield一个状态。使用协程可以实现高效的并发任务。Python的在3.4中引入了协程的概念,可是这个还是以生成器对象为基础,3.5则确定了协程的语法。下面将简单介绍asyncio的使用。实现协程的不仅仅是asyncio,tornado和gevent都实现了类似的功能。

event_loop 事件循环:程序开启一个无限的循环,程序员会把一些函数注册到事件循环上。当满足事件发生的时候,调用相应的协程函数。
coroutine 协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环,由事件循环调用。
task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。
future: 代表将来执行或没有执行的任务的结果。它和task上没有本质的区别
async/await 关键字:python3.5 用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口。
上述的概念单独拎出来都不好懂,比较他们之间是相互联系,一起工作。下面看例子,再回溯上述概念,更利于理解。

定义一个协程
定义一个协程很简单,使用async关键字,就像定义普通函数一样:

import time
import asyncio

now = lambda : time.time()

async def do_some_work(x):
print('Waiting: ', x)

start = now()

coroutine = do_some_work(2)

loop = asyncio.get_event_loop()
loop.run_until_complete(coroutine)

print('TIME: ', now() - start)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import time
import asyncio

now = lambda : time.time()

async def do_some_work(x):
print('Waiting: ', x)

start = now()

coroutine = do_some_work(2)

loop = asyncio.get_event_loop()
loop.run_until_complete(coroutine)

print('TIME: ', now() - start)
通过async关键字定义一个协程(coroutine),协程也是一种对象。协程不能直接运行,需要把协程加入到事件循环(loop),由后者在适当的时候调用协程。asyncio.get_event_loop方法可以创建一个事件循环,然后使用run_until_complete将协程注册到事件循环,并启动事件循环。因为本例只有一个协程,于是可以看见如下输出:

Waiting: 2
TIME: 0.0004658699035644531
1
2
Waiting: 2
TIME: 0.0004658699035644531
创建一个task
协程对象不能直接运行,在注册事件循环的时候,其实是run_until_complete方法将协程包装成为了一个任务(task)对象。所谓task对象是Future类的子类。保存了协程运行后的状态,用于未来获取协程的结果。

import asyncio
import time

now = lambda : time.time()

async def do_some_work(x):
print('Waiting: ', x)

start = now()

coroutine = do_some_work(2)
loop = asyncio.get_event_loop()

task = asyncio.ensure_future(coroutine)

task = loop.create_task(coroutine)
print(task)
loop.run_until_complete(task)
print(task)
print('TIME: ', now() - start)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import asyncio
import time

now = lambda : time.time()

async def do_some_work(x):
print('Waiting: ', x)

start = now()

coroutine = do_some_work(2)
loop = asyncio.get_event_loop()

task = asyncio.ensure_future(coroutine)

task = loop.create_task(coroutine)
print(task)
loop.run_until_complete(task)
print(task)
print('TIME: ', now() - start)
可以看到输出结果为:

Python

<Task pending coro=<do_some_work() running at /Users/ghost/Rsj217/python3.6/async/async-main.py:17>>
Waiting: 2
<Task finished coro=<do_some_work() done, defined at /Users/ghost/Rsj217/python3.6/async/async-main.py:17> result=None>
TIME: 0.0003490447998046875
1
2
3
4
<Task pending coro=<do_some_work() running at /Users/ghost/Rsj217/python3.6/async/async-main.py:17>>
Waiting: 2
<Task finished coro=<do_some_work() done, defined at /Users/ghost/Rsj217/python3.6/async/async-main.py:17> result=None>
TIME: 0.0003490447998046875
创建task后,task在加入事件循环之前是pending状态,因为do_some_work中没有耗时的阻塞操作,task很快就执行完毕了。后面打印的finished状态。

asyncio.ensure_future(coroutine) 和 loop.create_task(coroutine)都可以创建一个task,run_until_complete的参数是一个futrue对象。当传入一个协程,其内部会自动封装成task,task是Future的子类。isinstance(task, asyncio.Future)将会输出True。

绑定回调
绑定回调,在task执行完毕的时候可以获取执行的结果,回调的最后一个参数是future对象,通过该对象可以获取协程返回值。如果回调需要多个参数,可以通过偏函数导入。

import time
import asyncio

now = lambda : time.time()

async def do_some_work(x):
print('Waiting: ', x)
return ‘Done after {}s’.format(x)

def callback(future):
print('Callback: ', future.result())

start = now()

coroutine = do_some_work(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)
task.add_done_callback(callback)
loop.run_until_complete(task)

print('TIME: ', now() - start)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import time
import asyncio

now = lambda : time.time()

async def do_some_work(x):
print('Waiting: ', x)
return ‘Done after {}s’.format(x)

def callback(future):
print('Callback: ', future.result())

start = now()

coroutine = do_some_work(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)
task.add_done_callback(callback)
loop.run_until_complete(task)

print('TIME: ', now() - start)

def callback(t, future):
print(‘Callback:’, t, future.result())

task.add_done_callback(functools.partial(callback, 2))
1
2
3
4
def callback(t, future):
print(‘Callback:’, t, future.result())

task.add_done_callback(functools.partial(callback, 2))
可以看到,coroutine执行结束时候会调用回调函数。并通过参数future获取协程执行的结果。我们创建的task和回调里的future对象,实际上是同一个对象。

future 与 result
回调一直是很多异步编程的恶梦,程序员更喜欢使用同步的编写方式写异步代码,以避免回调的恶梦。回调中我们使用了future对象的result方法。前面不绑定回调的例子中,我们可以看到task有fiinished状态。在那个时候,可以直接读取task的result方法。

async def do_some_work(x):
print(‘Waiting {}’.format(x))
return ‘Done after {}s’.format(x)

start = now()

coroutine = do_some_work(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)
loop.run_until_complete(task)

print(‘Task ret: {}’.format(task.result()))
print(‘TIME: {}’.format(now() - start))
1
2
3
4
5
6
7
8
9
10
11
12
13
async def do_some_work(x):
print(‘Waiting {}’.format(x))
return ‘Done after {}s’.format(x)

start = now()

coroutine = do_some_work(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)
loop.run_until_complete(task)

print(‘Task ret: {}’.format(task.result()))
print(‘TIME: {}’.format(now() - start))
可以看到输出的结果:

Waiting: 2
Task ret: Done after 2s
TIME: 0.0003650188446044922
1
2
3
Waiting: 2
Task ret: Done after 2s
TIME: 0.0003650188446044922
阻塞和await
使用async可以定义协程对象,使用await可以针对耗时的操作进行挂起,就像生成器里的yield一样,函数让出控制权。协程遇到await,事件循环将会挂起该协程,执行别的协程,直到其他的协程也挂起或者执行完毕,再进行下一个协程的执行。

耗时的操作一般是一些IO操作,例如网络请求,文件读取等。我们使用asyncio.sleep函数来模拟IO操作。协程的目的也是让这些IO操作异步化。

import asyncio
import time

now = lambda: time.time()

async def do_some_work(x):
print('Waiting: ', x)
await asyncio.sleep(x)
return ‘Done after {}s’.format(x)

start = now()

coroutine = do_some_work(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)
loop.run_until_complete(task)

print('Task ret: ', task.result())
print('TIME: ', now() - start)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import asyncio
import time

now = lambda: time.time()

async def do_some_work(x):
print('Waiting: ', x)
await asyncio.sleep(x)
return ‘Done after {}s’.format(x)

start = now()

coroutine = do_some_work(2)
loop = asyncio.get_event_loop()
task = asyncio.ensure_future(coroutine)
loop.run_until_complete(task)

print('Task ret: ', task.result())
print('TIME: ', now() - start)
在 sleep的时候,使用await让出控制权。即当遇到阻塞调用的函数的时候,使用await方法将协程的控制权让出,以便loop调用其他的协程。现在我们的例子就用耗时的阻塞操作了。

并发和并行
并发和并行一直是容易混淆的概念。并发通常指有多个任务需要同时进行,并行则是同一时刻有多个任务执行。用上课来举例就是,并发情况下是一个老师在同一时间段辅助不同的人功课。并行则是好几个老师分别同时辅助多个学生功课。简而言之就是一个人同时吃三个馒头还是三个人同时分别吃一个的情况,吃一个馒头算一个任务。

asyncio实现并发,就需要多个协程来完成任务,每当有任务阻塞的时候就await,然后其他协程继续工作。创建多个协程的列表,然后将这些协程注册到事件循环中。

import asyncio

import time

now = lambda: time.time()

async def do_some_work(x):
print('Waiting: ', x)

await asyncio.sleep(x)
return 'Done after {}s'.format(x)

start = now()

coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)

tasks = [
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2),
asyncio.ensure_future(coroutine3)
]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

for task in tasks:
print('Task ret: ', task.result())

print('TIME: ', now() - start)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import asyncio

import time

now = lambda: time.time()

async def do_some_work(x):
print('Waiting: ', x)

await asyncio.sleep(x)
return 'Done after {}s'.format(x)

start = now()

coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)

tasks = [
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2),
asyncio.ensure_future(coroutine3)
]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

for task in tasks:
print('Task ret: ', task.result())

print('TIME: ', now() - start)
结果如下

Waiting: 1
Waiting: 2
Waiting: 4
Task ret: Done after 1s
Task ret: Done after 2s
Task ret: Done after 4s
TIME: 4.003541946411133
1
2
3
4
5
6
7
Waiting: 1
Waiting: 2
Waiting: 4
Task ret: Done after 1s
Task ret: Done after 2s
Task ret: Done after 4s
TIME: 4.003541946411133
总时间为4s左右。4s的阻塞时间,足够前面两个协程执行完毕。如果是同步顺序的任务,那么至少需要7s。此时我们使用了aysncio实现了并发。asyncio.wait(tasks) 也可以使用 asyncio.gather(*tasks) ,前者接受一个task列表,后者接收一堆task。

协程嵌套
使用async可以定义协程,协程用于耗时的io操作,我们也可以封装更多的io操作过程,这样就实现了嵌套的协程,即一个协程中await了另外一个协程,如此连接起来。

import asyncio

import time

now = lambda: time.time()

async def do_some_work(x):
print('Waiting: ', x)

await asyncio.sleep(x)
return 'Done after {}s'.format(x)

async def main():
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)

tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]

dones, pendings = await asyncio.wait(tasks)

for task in dones:
    print('Task ret: ', task.result())

start = now()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

print('TIME: ', now() - start)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import asyncio

import time

now = lambda: time.time()

async def do_some_work(x):
print('Waiting: ', x)

await asyncio.sleep(x)
return 'Done after {}s'.format(x)

async def main():
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)

tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]

dones, pendings = await asyncio.wait(tasks)

for task in dones:
    print('Task ret: ', task.result())

start = now()

loop = asyncio.get_event_loop()
loop.run_until_complete(main())

print('TIME: ', now() - start)
如果使用的是 asyncio.gather创建协程对象,那么await的返回值就是协程运行的结果。

results = await asyncio.gather(*tasks)

for result in results:
    print('Task ret: ', result)

1
2
3
4
results = await asyncio.gather(*tasks)

for result in results:
    print('Task ret: ', result)

不在main协程函数里处理结果,直接返回await的内容,那么最外层的run_until_complete将会返回main协程的结果。

async def main():
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(2)

tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]

return await asyncio.gather(*tasks)

start = now()

loop = asyncio.get_event_loop()
results = loop.run_until_complete(main())

for result in results:
print('Task ret: ', result)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async def main():
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(2)

tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]

return await asyncio.gather(*tasks)

start = now()

loop = asyncio.get_event_loop()
results = loop.run_until_complete(main())

for result in results:
print('Task ret: ', result)
或者返回使用asyncio.wait方式挂起协程。

async def main():
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)

tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]

return await asyncio.wait(tasks)

start = now()

loop = asyncio.get_event_loop()
done, pending = loop.run_until_complete(main())

for task in done:
print('Task ret: ', task.result())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
async def main():
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)

tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]

return await asyncio.wait(tasks)

start = now()

loop = asyncio.get_event_loop()
done, pending = loop.run_until_complete(main())

for task in done:
print('Task ret: ', task.result())
也可以使用asyncio的as_completed方法

async def main():
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)

tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]
for task in asyncio.as_completed(tasks):
    result = await task
    print('Task ret: {}'.format(result))

start = now()

loop = asyncio.get_event_loop()
done = loop.run_until_complete(main())
print('TIME: ', now() - start)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async def main():
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(4)

tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]
for task in asyncio.as_completed(tasks):
    result = await task
    print('Task ret: {}'.format(result))

start = now()

loop = asyncio.get_event_loop()
done = loop.run_until_complete(main())
print('TIME: ', now() - start)
由此可见,协程的调用和组合十分灵活,尤其是对于结果的处理,如何返回,如何挂起,需要逐渐积累经验和前瞻的设计。

协程停止
上面见识了协程的几种常用的用法,都是协程围绕着事件循环进行的操作。future对象有几个状态:

Pending
Running
Done
Cancelled
创建future的时候,task为pending,事件循环调用执行的时候当然就是running,调用完毕自然就是done,如果需要停止事件循环,就需要先把task取消。可以使用asyncio.Task获取事件循环的task

import asyncio

import time

now = lambda: time.time()

async def do_some_work(x):
print('Waiting: ', x)

await asyncio.sleep(x)
return 'Done after {}s'.format(x)

coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(2)

tasks = [
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2),
asyncio.ensure_future(coroutine3)
]

start = now()

loop = asyncio.get_event_loop()
try:
loop.run_until_complete(asyncio.wait(tasks))
except KeyboardInterrupt as e:
print(asyncio.Task.all_tasks())
for task in asyncio.Task.all_tasks():
print(task.cancel())
loop.stop()
loop.run_forever()
finally:
loop.close()

print('TIME: ', now() - start)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import asyncio

import time

now = lambda: time.time()

async def do_some_work(x):
print('Waiting: ', x)

await asyncio.sleep(x)
return 'Done after {}s'.format(x)

coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(2)

tasks = [
asyncio.ensure_future(coroutine1),
asyncio.ensure_future(coroutine2),
asyncio.ensure_future(coroutine3)
]

start = now()

loop = asyncio.get_event_loop()
try:
loop.run_until_complete(asyncio.wait(tasks))
except KeyboardInterrupt as e:
print(asyncio.Task.all_tasks())
for task in asyncio.Task.all_tasks():
print(task.cancel())
loop.stop()
loop.run_forever()
finally:
loop.close()

print('TIME: ', now() - start)
启动事件循环之后,马上ctrl+c,会触发run_until_complete的执行异常 KeyBorardInterrupt。然后通过循环asyncio.Task取消future。可以看到输出如下:

Python

Waiting: 1
Waiting: 2
Waiting: 2
{<Task pending coro=<do_some_work() running at /Users/ghost/Rsj217/python3.6/async/async-main.py:18> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x101230648>()]> cb=[_wait.._on_completion() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:374]>, <Task pending coro=<do_some_work() running at /Users/ghost/Rsj217/python3.6/async/async-main.py:18> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x1032b10a8>()]> cb=[_wait.._on_completion() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:374]>, <Task pending coro=<wait() running at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:307> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x103317d38>()]> cb=[_run_until_complete_cb() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py:176]>, <Task pending coro=<do_some_work() running at /Users/ghost/Rsj217/python3.6/async/async-main.py:18> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x103317be8>()]> cb=[_wait.._on_completion() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:374]>}
True
True
True
True
TIME: 0.8858370780944824
1
2
3
4
5
6
7
8
9
Waiting: 1
Waiting: 2
Waiting: 2
{<Task pending coro=<do_some_work() running at /Users/ghost/Rsj217/python3.6/async/async-main.py:18> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x101230648>()]> cb=[_wait.._on_completion() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:374]>, <Task pending coro=<do_some_work() running at /Users/ghost/Rsj217/python3.6/async/async-main.py:18> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x1032b10a8>()]> cb=[_wait.._on_completion() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:374]>, <Task pending coro=<wait() running at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:307> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x103317d38>()]> cb=[_run_until_complete_cb() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py:176]>, <Task pending coro=<do_some_work() running at /Users/ghost/Rsj217/python3.6/async/async-main.py:18> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x103317be8>()]> cb=[_wait.._on_completion() at /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/tasks.py:374]>}
True
True
True
True
TIME: 0.8858370780944824
True表示cannel成功,loop stop之后还需要再次开启事件循环,最后在close,不然还会抛出异常:

Python

Task was destroyed but it is pending!
task: <Task pending coro=<do_some_work() done,
1
2
Task was destroyed but it is pending!
task: <Task pending coro=<do_some_work() done,
循环task,逐个cancel是一种方案,可是正如上面我们把task的列表封装在main函数中,main函数外进行事件循环的调用。这个时候,main相当于最外出的一个task,那么处理包装的main函数即可。

import asyncio

import time

now = lambda: time.time()

async def do_some_work(x):
print('Waiting: ', x)

await asyncio.sleep(x)
return 'Done after {}s'.format(x)

async def main():
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(2)

tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]
done, pending = await asyncio.wait(tasks)
for task in done:
    print('Task ret: ', task.result())

start = now()

loop = asyncio.get_event_loop()
task = asyncio.ensure_future(main())
try:
loop.run_until_complete(task)
except KeyboardInterrupt as e:
print(asyncio.Task.all_tasks())
print(asyncio.gather(*asyncio.Task.all_tasks()).cancel())
loop.stop()
loop.run_forever()
finally:
loop.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import asyncio

import time

now = lambda: time.time()

async def do_some_work(x):
print('Waiting: ', x)

await asyncio.sleep(x)
return 'Done after {}s'.format(x)

async def main():
coroutine1 = do_some_work(1)
coroutine2 = do_some_work(2)
coroutine3 = do_some_work(2)

tasks = [
    asyncio.ensure_future(coroutine1),
    asyncio.ensure_future(coroutine2),
    asyncio.ensure_future(coroutine3)
]
done, pending = await asyncio.wait(tasks)
for task in done:
    print('Task ret: ', task.result())

start = now()

loop = asyncio.get_event_loop()
task = asyncio.ensure_future(main())
try:
loop.run_until_complete(task)
except KeyboardInterrupt as e:
print(asyncio.Task.all_tasks())
print(asyncio.gather(*asyncio.Task.all_tasks()).cancel())
loop.stop()
loop.run_forever()
finally:
loop.close()
不同线程的事件循环
很多时候,我们的事件循环用于注册协程,而有的协程需要动态的添加到事件循环中。一个简单的方式就是使用多线程。当前线程创建一个事件循环,然后在新建一个线程,在新线程中启动事件循环。当前线程不会被block。

from threading import Thread

def start_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()

def more_work(x):
print(‘More work {}’.format(x))
time.sleep(x)
print(‘Finished more work {}’.format(x))

start = now()
new_loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(new_loop,))
t.start()
print(‘TIME: {}’.format(time.time() - start))

new_loop.call_soon_threadsafe(more_work, 6)
new_loop.call_soon_threadsafe(more_work, 3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from threading import Thread

def start_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()

def more_work(x):
print(‘More work {}’.format(x))
time.sleep(x)
print(‘Finished more work {}’.format(x))

start = now()
new_loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(new_loop,))
t.start()
print(‘TIME: {}’.format(time.time() - start))

new_loop.call_soon_threadsafe(more_work, 6)
new_loop.call_soon_threadsafe(more_work, 3)
启动上述代码之后,当前线程不会被block,新线程中会按照顺序执行call_soon_threadsafe方法注册的more_work方法,后者因为time.sleep操作是同步阻塞的,因此运行完毕more_work需要大致6 + 3

新线程协程

def start_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()

async def do_some_work(x):
print(‘Waiting {}’.format(x))
await asyncio.sleep(x)
print(‘Done after {}s’.format(x))

def more_work(x):
print(‘More work {}’.format(x))
time.sleep(x)
print(‘Finished more work {}’.format(x))

start = now()
new_loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(new_loop,))
t.start()
print(‘TIME: {}’.format(time.time() - start))

asyncio.run_coroutine_threadsafe(do_some_work(6), new_loop)
asyncio.run_coroutine_threadsafe(do_some_work(4), new_loop)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def start_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()

async def do_some_work(x):
print(‘Waiting {}’.format(x))
await asyncio.sleep(x)
print(‘Done after {}s’.format(x))

def more_work(x):
print(‘More work {}’.format(x))
time.sleep(x)
print(‘Finished more work {}’.format(x))

start = now()
new_loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(new_loop,))
t.start()
print(‘TIME: {}’.format(time.time() - start))

asyncio.run_coroutine_threadsafe(do_some_work(6), new_loop)
asyncio.run_coroutine_threadsafe(do_some_work(4), new_loop)
上述的例子,主线程中创建一个new_loop,然后在另外的子线程中开启一个无限事件循环。主线程通过run_coroutine_threadsafe新注册协程对象。这样就能在子线程中进行事件循环的并发操作,同时主线程又不会被block。一共执行的时间大概在6s左右。

master-worker主从模式
对于并发任务,通常是用生成消费模型,对队列的处理可以使用类似master-worker的方式,master主要用户获取队列的msg,worker用户处理消息。

为了简单起见,并且协程更适合单线程的方式,我们的主线程用来监听队列,子线程用于处理队列。这里使用redis的队列。主线程中有一个是无限循环,用户消费队列。

while True:
    task = rcon.rpop("queue")
    if not task:
        time.sleep(1)
        continue
    asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)

1
2
3
4
5
6
while True:
task = rcon.rpop(“queue”)
if not task:
time.sleep(1)
continue
asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)
给队列添加一些数据:

127.0.0.1:6379[3]> lpush queue 2
(integer) 1
127.0.0.1:6379[3]> lpush queue 5
(integer) 1
127.0.0.1:6379[3]> lpush queue 1
(integer) 1
127.0.0.1:6379[3]> lpush queue 1
1
2
3
4
5
6
7
127.0.0.1:6379[3]> lpush queue 2
(integer) 1
127.0.0.1:6379[3]> lpush queue 5
(integer) 1
127.0.0.1:6379[3]> lpush queue 1
(integer) 1
127.0.0.1:6379[3]> lpush queue 1
可以看见输出:

Waiting 2
Done 2
Waiting 5
Waiting 1
Done 1
Waiting 1
Done 1
Done 5
1
2
3
4
5
6
7
8
Waiting 2
Done 2
Waiting 5
Waiting 1
Done 1
Waiting 1
Done 1
Done 5
我们发起了一个耗时5s的操作,然后又发起了连个1s的操作,可以看见子线程并发的执行了这几个任务,其中5s awati的时候,相继执行了1s的两个任务。

停止子线程
如果一切正常,那么上面的例子很完美。可是,需要停止程序,直接ctrl+c,会抛出KeyboardInterrupt错误,我们修改一下主循环:

try:
while True:
task = rcon.rpop(“queue”)
if not task:
time.sleep(1)
continue
asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)
except KeyboardInterrupt as e:
print(e)
new_loop.stop()
1
2
3
4
5
6
7
8
9
10
try:
while True:
task = rcon.rpop(“queue”)
if not task:
time.sleep(1)
continue
asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)
except KeyboardInterrupt as e:
print(e)
new_loop.stop()
可是实际上并不好使,虽然主线程try了KeyboardInterrupt异常,但是子线程并没有退出,为了解决这个问题,可以设置子线程为守护线程,这样当主线程结束的时候,子线程也随机退出。

new_loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(new_loop,))
t.setDaemon(True) # 设置子线程为守护线程
t.start()

try:
while True:
# print(‘start rpop’)
task = rcon.rpop(“queue”)
if not task:
time.sleep(1)
continue
asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)
except KeyboardInterrupt as e:
print(e)
new_loop.stop()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new_loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(new_loop,))
t.setDaemon(True) # 设置子线程为守护线程
t.start()

try:
while True:
# print(‘start rpop’)
task = rcon.rpop(“queue”)
if not task:
time.sleep(1)
continue
asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)
except KeyboardInterrupt as e:
print(e)
new_loop.stop()
线程停止程序的时候,主线程退出后,子线程也随机退出才了,并且停止了子线程的协程任务。

aiohttp
在消费队列的时候,我们使用asyncio的sleep用于模拟耗时的io操作。以前有一个短信服务,需要在协程中请求远程的短信api,此时需要是需要使用aiohttp进行异步的http请求。大致代码如下:

server.py

Python

import time
from flask import Flask, request

app = Flask(name)

@app.route(’/int:x’)
def index(x):
time.sleep(x)
return “{} It works”.format(x)

@app.route(’/error’)
def error():
time.sleep(3)
return “error!”

if name == ‘main’:
app.run(debug=True)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import time
from flask import Flask, request

app = Flask(name)

@app.route(’/int:x’)
def index(x):
time.sleep(x)
return “{} It works”.format(x)

@app.route(’/error’)
def error():
time.sleep(3)
return “error!”

if name == ‘main’:
app.run(debug=True)
/接口表示短信接口,/error表示请求/失败之后的报警。

async-custoimer.py

import time
import asyncio
from threading import Thread
import redis
import aiohttp

def get_redis():
connection_pool = redis.ConnectionPool(host=‘127.0.0.1’, db=3)
return redis.Redis(connection_pool=connection_pool)

rcon = get_redis()

def start_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()

async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
print(resp.status)
return await resp.text()

async def do_some_work(x):
print('Waiting ', x)
try:
ret = await fetch(url=‘http://127.0.0.1:5000/{}’.format(x))
print(ret)
except Exception as e:
try:
print(await fetch(url=‘http://127.0.0.1:5000/error’))
except Exception as e:
print(e)
else:
print(‘Done {}’.format(x))

new_loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(new_loop,))
t.setDaemon(True)
t.start()

try:
while True:
task = rcon.rpop(“queue”)
if not task:
time.sleep(1)
continue
asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)
except Exception as e:
print(‘error’)
new_loop.stop()
finally:
pass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import time
import asyncio
from threading import Thread
import redis
import aiohttp

def get_redis():
connection_pool = redis.ConnectionPool(host=‘127.0.0.1’, db=3)
return redis.Redis(connection_pool=connection_pool)

rcon = get_redis()

def start_loop(loop):
asyncio.set_event_loop(loop)
loop.run_forever()

async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
print(resp.status)
return await resp.text()

async def do_some_work(x):
print('Waiting ', x)
try:
ret = await fetch(url=‘http://127.0.0.1:5000/{}’.format(x))
print(ret)
except Exception as e:
try:
print(await fetch(url=‘http://127.0.0.1:5000/error’))
except Exception as e:
print(e)
else:
print(‘Done {}’.format(x))

new_loop = asyncio.new_event_loop()
t = Thread(target=start_loop, args=(new_loop,))
t.setDaemon(True)
t.start()

try:
while True:
task = rcon.rpop(“queue”)
if not task:
time.sleep(1)
continue
asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)
except Exception as e:
print(‘error’)
new_loop.stop()
finally:
pass
有一个问题需要注意,我们在fetch的时候try了异常,如果没有try这个异常,即使发生了异常,子线程的事件循环也不会退出。主线程也不会退出,暂时没找到办法可以把子线程的异常raise传播到主线程。(如果谁找到了比较好的方式,希望可以带带我)。

对于redis的消费,还有一个block的方法:

try:
while True:
_, task = rcon.brpop(“queue”)
asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)
except Exception as e:
print(‘error’, e)
new_loop.stop()
finally:
pass
1
2
3
4
5
6
7
8
9
try:
while True:
_, task = rcon.brpop(“queue”)
asyncio.run_coroutine_threadsafe(do_some_work(int(task)), new_loop)
except Exception as e:
print(‘error’, e)
new_loop.stop()
finally:
pass
使用 brpop方法,会block住task,如果主线程有消息,才会消费。测试了一下,似乎brpop的方式更适合这种队列消费的模型。

127.0.0.1:6379[3]> lpush queue 5
(integer) 1
127.0.0.1:6379[3]> lpush queue 1
(integer) 1
127.0.0.1:6379[3]> lpush queue 1
1
2
3
4
5
127.0.0.1:6379[3]> lpush queue 5
(integer) 1
127.0.0.1:6379[3]> lpush queue 1
(integer) 1
127.0.0.1:6379[3]> lpush queue 1
可以看到结果

Waiting 5
Waiting 1
Waiting 1
200
1 It works
Done 1
200
1 It works
Done 1
200
5 It works
Done 5
1
2
3
4
5
6
7
8
9
10
11
12
Waiting 5
Waiting 1
Waiting 1
200
1 It works
Done 1
200
1 It works
Done 1
200
5 It works
Done 5
协程消费
主线程用于监听队列,然后子线程的做事件循环的worker是一种方式。还有一种方式实现这种类似master-worker的方案。即把监听队列的无限循环逻辑一道协程中。程序初始化就创建若干个协程,实现类似并行的效果。

import time
import asyncio
import redis

now = lambda : time.time()

def get_redis():
connection_pool = redis.ConnectionPool(host=‘127.0.0.1’, db=3)
return redis.Redis(connection_pool=connection_pool)

rcon = get_redis()

async def worker():
print(‘Start worker’)

while True:
    start = now()
    task = rcon.rpop("queue")
    if not task:
        await asyncio.sleep(1)
        continue
    print('Wait ', int(task))
    await asyncio.sleep(int(task))
    print('Done ', task, now() - start)

def main():
asyncio.ensure_future(worker())
asyncio.ensure_future(worker())

loop = asyncio.get_event_loop()
try:
    loop.run_forever()
except KeyboardInterrupt as e:
    print(asyncio.gather(*asyncio.Task.all_tasks()).cancel())
    loop.stop()
    loop.run_forever()
finally:
    loop.close()

if name == ‘main’:
main()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import time
import asyncio
import redis

now = lambda : time.time()

def get_redis():
connection_pool = redis.ConnectionPool(host=‘127.0.0.1’, db=3)
return redis.Redis(connection_pool=connection_pool)

rcon = get_redis()

async def worker():
print(‘Start worker’)

while True:
    start = now()
    task = rcon.rpop("queue")
    if not task:
        await asyncio.sleep(1)
        continue
    print('Wait ', int(task))
    await asyncio.sleep(int(task))
    print('Done ', task, now() - start)

def main():
asyncio.ensure_future(worker())
asyncio.ensure_future(worker())

loop = asyncio.get_event_loop()
try:
    loop.run_forever()
except KeyboardInterrupt as e:
    print(asyncio.gather(*asyncio.Task.all_tasks()).cancel())
    loop.stop()
    loop.run_forever()
finally:
    loop.close()

if name == ‘main’:
main()
这样做就可以多多启动几个worker来监听队列。一样可以到达效果。

总结
上述简单的介绍了asyncio的用法,主要是理解事件循环,协程和任务,future的关系。异步编程不同于常见的同步编程,设计程序的执行流的时候,需要特别的注意。比较这和以往的编码经验有点不一样。可是仔细想想,我们平时处事的时候,大脑会自然而然的实现异步协程。比如等待煮茶的时候,可以多写几行代码。

相关代码文件的Gist

参考:Threaded Asynchronous Magic and How to Wield It

------------------------------------------------文章3:-----------------------------------------------------
作者:goodspeed
2017-06-14 发布
python协程2:yield from 从入门到精通
协程 python 9.4k 次阅读 · 读完需要 34 分钟
7
上一篇python协程1:yield的使用介绍了:

生成器作为协程使用时的行为和状态

使用装饰器预激协程

调用方如何使用生成器对象的 .throw(…) 和 .close() 方法控制协程

这一篇将介绍:

协程终止时如何返回值

yield新句法的用途和语义

同时会用几个协程的示例展示协程用法。

让协程返回值
先看一个例子:
这段代码会返回最终均值的结果,每次激活协程时不会产出移动平均值,而是最后一次返回。

#! -- coding: utf-8 --

from collections import namedtuple

Result = namedtuple(‘Result’, ‘count average’)

def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
if term is None:
break # 为了返回值,协程必须正常终止;这里是退出条件
total += term
count += 1
average = total/count
# 返回一个namedtuple,包含count和average两个字段。在python3.3前,如果生成器返回值,会报错
return Result(count, average)
我们调用这段代码,结果如下

coro_avg = averager()
next(coro_avg)
coro_avg.send(20) # 并没有返回值
coro_avg.send(30)
coro_avg.send(40)
coro_avg.send(None) # 发送None终止循环,导致协程结束。生成器对象会抛出StopIteration异常。异常对象的value属性保存着返回值。
Traceback (most recent call last):

StopIteration: Result(count=3, average=30)
return 表达式的值会传给调用方,赋值给StopIteration 异常的一个属性。这样做虽然看着别扭,但为了保留生成器对象耗尽时抛出StopIteration异常的行为,也可以理解。

如果我们想获取协程的返回值,可以这么操作:

coro_avg = averager()
next(coro_avg)
coro_avg.send(20) # 并没有返回值
coro_avg.send(30)
coro_avg.send(40)
try:
… coro_avg.send(None)
… except StopIteration as exc:
… result = exc.value

result
Result(count=3, average=30)
看到这我们会说,这是什么鬼,为什么获取返回值要绕这么一大圈,就没有简单的方法吗?

有的,那就是 yield from

yield from 结果会在内部自动捕获StopIteration 异常。这种处理方式与 for 循环处理StopIteration异常的方式一样。
对于yield from 结构来说,解释器不仅会捕获StopIteration异常,还会把value属性的值变成yield from 表达式的值。

在函数外部不能使用yield from(yield也不行)。

既然我们提到了 yield from 那yield from 是什么呢?

yield from
yield from 是 Python3.3 后新加的语言结构。和其他语言的await关键字类似,它表示:*在生成器 gen 中使用 yield from subgen()时,subgen 会获得控制权,把产出的值传个gen的调用方,即调用方可以直接控制subgen。于此同时,gen会阻塞,等待subgen终止。

yield from 可用于简化for循环中的yield表达式。

例如:

def gen():
for c in ‘AB’:
yield c
for i in range(1, 3):
yield i

list(gen())
[‘A’, ‘B’, ‘1’, ‘2’]
可以改写为:

def gen():
yield from ‘AB’
yield from range(1, 3)

list(gen())
[‘A’, ‘B’, ‘1’, ‘2’]
下面来看一个复杂点的例子:(来自Python cookbook 3 ,github源码地址 https://github.com/dabeaz/python-cookbook/blob/master/src/4/how_to_flatten_a_nested_sequence/example.py)

Example of flattening a nested sequence using subgenerators

from collections import Iterable

def flatten(items, ignore_types=(str, bytes)):
for x in items:
if isinstance(x, Iterable) and not isinstance(x, ignore_types):
yield from flatten(x) # 这里递归调用,如果x是可迭代对象,继续分解
else:
yield x

items = [1, 2, [3, 4, [5, 6], 7], 8]

Produces 1 2 3 4 5 6 7 8

for x in flatten(items):
print(x)

items = [‘Dave’, ‘Paula’, [‘Thomas’, ‘Lewis’]]
for x in flatten(items):
print(x)
yield from x 表达式对x对象做的第一件事是,调用 iter(x),获取迭代器。所以要求x是可迭代对象。

PEP380 的标题是 ”syntax for delegating to subgenerator“(把指责委托给子生成器的句法)。由此我们可以知道,yield from是可以实现嵌套生成器的使用。

yield from 的主要功能是打开双向通道,把最外层的调用方与最内层的子生成器连接起来,使两者可以直接发送和产出值,还可以直接传入异常,而不用在中间的协程添加异常处理的代码。

yield from 包含几个概念:

委派生成器

包含yield from 表达式的生成器函数

子生成器

从yield from 部分获取的生成器。

调用方

调用委派生成器的客户端(调用方)代码

这个示意图是 对yield from 的调用过程

委派生成器在 yield from 表达式处暂停时,调用方可以直接把数据发给字生成器,子生成器再把产出的值发送给调用方。子生成器返回之后,解释器会抛出StopIteration异常,并把返回值附加到异常对象上,只是委派生成器恢复。

这个图来自于Paul

Sokolovsky 的 How Python 3.3 “yield from” construct works

下边这个例子是对yield from 的一个应用:

#! -- coding: utf-8 --

from collections import namedtuple

Result = namedtuple(‘Result’, ‘count average’)

子生成器

这个例子和上边示例中的 averager 协程一样,只不过这里是作为字生成器使用

def averager():
total = 0.0
count = 0
average = None
while True:
# main 函数发送数据到这里
term = yield
if term is None: # 终止条件
break
total += term
count += 1
average = total/count
return Result(count, average) # 返回的Result 会成为grouper函数中yield from表达式的值

委派生成器

def grouper(results, key):
# 这个循环每次都会新建一个averager 实例,每个实例都是作为协程使用的生成器对象
while True:
# grouper 发送的每个值都会经由yield from 处理,通过管道传给averager 实例。grouper会在yield from表达式处暂停,等待averager实例处理客户端发来的值。averager实例运行完毕后,返回的值绑定到results[key] 上。while 循环会不断创建averager实例,处理更多的值。
results[key] = yield from averager()

调用方

def main(data):
results = {}
for key, values in data.items():
# group 是调用grouper函数得到的生成器对象,传给grouper 函数的第一个参数是results,用于收集结果;第二个是某个键
group = grouper(results, key)
next(group)
for value in values:
# 把各个value传给grouper 传入的值最终到达averager函数中;
# grouper并不知道传入的是什么,同时grouper实例在yield from处暂停
group.send(value)
# 把None传入groupper,传入的值最终到达averager函数中,导致当前实例终止。然后继续创建下一个实例。
# 如果没有group.send(None),那么averager子生成器永远不会终止,委派生成器也永远不会在此激活,也就不会为result[key]赋值
group.send(None)
report(results)

输出报告

def report(results):
for key, result in sorted(results.items()):
group, unit = key.split(’;’)
print(’{:2} {:5} averaging {:.2f}{}’.format(result.count, group, result.average, unit))

data = {
‘girls;kg’:[40, 41, 42, 43, 44, 54],
‘girls;m’: [1.5, 1.6, 1.8, 1.5, 1.45, 1.6],
‘boys;kg’:[50, 51, 62, 53, 54, 54],
‘boys;m’: [1.6, 1.8, 1.8, 1.7, 1.55, 1.6],
}

if name == ‘main’:
main(data)
这段代码从一个字典中读取男生和女生的身高和体重。然后把数据传给之前定义的 averager 协程,最后生成一个报告。

执行结果为

6 boys averaging 54.00kg
6 boys averaging 1.68m
6 girls averaging 44.00kg
6 girls averaging 1.58m
这断代码展示了yield from 结构最简单的用法。委派生成器相当于管道,所以可以把任意数量的委派生成器连接在一起—一个委派生成器使用yield from 调用一个子生成器,而那个子生成器本身也是委派生成器,使用yield from调用另一个生成器。最终以一个只是用yield表达式的生成器(或者任意可迭代对象)结束。

yield from 的意义
PEP380 分6点说明了yield from 的行为。

子生成器产出的值都直接传给委派生成器的调用方(客户端代码)

使用send() 方法发给委派生成器的值都直接传给子生成器。如果发送的值是None,那么会调用子生成器的 next()方法。如果发送的值不是None,那么会调用子生成器的send()方法。如果调用的方法抛出StopIteration异常,那么委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。

生成器退出时,生成器(或子生成器)中的return expr 表达式会触发 StopIteration(expr) 异常抛出。

yield from表达式的值是子生成器终止时传给StopIteration异常的第一个参数。

传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的throw()方法。如果调用throw()方法时抛出 StopIteration 异常,委派生成器恢复运行。StopIteration之外的异常会向上冒泡。传给委派生成器。

如果把 GeneratorExit 异常传入委派生成器,或者在委派生成器上调用close() 方法,那么在子生成器上调用close() 方法,如果他有的话。如果调用close() 方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出 GeneratorExit 异常。

yield from的具体语义很难理解,不过我们可以看下Greg Ewing 的伪代码,通过伪代码分析一下:

RESULT = yield from EXPR

is semantically equivalent to

EXPR 可以是任何可迭代对象,因为获取迭代器_i 使用的是iter()函数。

_i = iter(EXPR)
try:
_y = next(_i) # 2 预激字生成器,结果保存在_y 中,作为第一个产出的值
except StopIteration as _e:
# 3 如果调用的方法抛出StopIteration异常,获取异常对象的value属性,赋值给_r
_r = _e.value
else:
while 1: # 4 运行这个循环时,委派生成器会阻塞,只能作为调用方和子生成器直接的通道
try:
_s = yield _y # 5 产出子生成器当前产出的元素;等待调用方发送_s中保存的值。
except GeneratorExit as _e:
# 6 这一部分是用于关闭委派生成器和子生成器,因为子生成器可以是任意可迭代对象,所以可能没有close() 方法。
try:
_m = _i.close
except AttributeError:
pass
else:
_m()
# 如果调用close() 方法导致异常抛出,那么异常会向上冒泡,传给委派生成器;否则,委派生成器抛出 GeneratorExit 异常。
raise _e
except BaseException as _e: # 7 这一部分处理调用方通过.throw() 方法传入的异常。如果子生成器是迭代器,没有throw()方法,这种情况会导致委派生成器抛出异常
_x = sys.exc_info()
try:
# 传入委派生成器的异常,除了 GeneratorExit 之外都传给子生成器的throw()方法。
_m = _i.throw
except AttributeError:
# 子生成器一迭代器,没有throw()方法, 调用throw()方法时抛出AttributeError异常传给委派生成器
raise _e
else: # 8
try:
_y = _m(*_x)
except StopIteration as _e:
# 如果调用throw()方法时抛出 StopIteration 异常,委派生成器恢复运行。
# StopIteration之外的异常会向上冒泡。传给委派生成器。
_r = _e.value
break
else: # 9 如果产出值时没有异常
try: # 10 尝试让子生成器向前执行
if _s is None:
# 11. 如果发送的值是None,那么会调用子生成器的 next()方法。
_y = next(_i)
else:
# 11. 如果发送的值不是None,那么会调用子生成器的send()方法。
_y = _i.send(_s)
except StopIteration as _e: # 12
# 2. 如果调用的方法抛出StopIteration异常,获取异常对象的value属性,赋值给_r, 退出循环,委派生成器恢复运行。任何其他异常都会向上冒泡,传给委派生成器。
_r = _e.value
break
RESULT = _r #13 返回的结果是 _r 即整个yield from表达式的值
上段代码变量说明:

_i 迭代器(子生成器)

_y 产出的值 (子生成器产出的值)

_r 结果 (最终的结果 即整个yield from表达式的值)

_s 发送的值 (调用方发给委派生成器的值,这个只会传给子生成器)

_e 异常 (异常对象)

我们可以看到在代码的第一个 try 部分 使用 _y = next(_i) 预激了子生成器。这可以看出,上一篇我们使用的用于自动预激的装饰器与yield from 语句不兼容。

除了这段伪代码之外,PEP380 还有个说明:

In a generator, the statement

return value

is semantically equivalent to

raise StopIteration(value)

except that, as currently, the exception cannot be caught by except clauses within the returning generator.
这也就是为什么 yield from 可以使用return 来返回值而 yield 只能使用 try … except StopIteration … 来捕获异常的value 值。

try:
… coro_avg.send(None)
… except StopIteration as exc:
… result = exc.value

result
到这里,我们已经了解了 yield from 的具体细节。下一篇,会分析一个使用协程的经典案例: 仿真编程。这个案例说明了如何使用协程在单线程中管理并发活动。

参考文档
流畅的python 第16章(这是读书笔记,这是读书笔记)

PEP 380-- Syntax for Delegating to a Subgenerator

How Python 3.3 “yield from” construct works

最后,感谢女朋友支持。

-------------------------------------------------3的前篇--------------------------------------------------
python协程1:yield的使用
goodspeed 四月 2017-06-13
最近找到一本python好书《流畅的python》,是到现在为止看到的对python高级特性讲述最详细的一本。
看了协程一章,做个读书笔记,加深印象。

协程定义

协程的底层架构是在pep342 中定义,并在python2.5 实现的。

python2.5 中,yield关键字可以在表达式中使用,而且生成器API中增加了 .send(value)方法。生成器可以使用.send(…)方法发送数据,发送的数据会成为生成器函数中yield表达式的值。

协程是指一个过程,这个过程与调用方协作,产出有调用方提供的值。因此,生成器可以作为协程使用。

除了 .send(…)方法,pep342 和添加了 .throw(…)(让调用方抛出异常,在生成器中处理)和.close()(终止生成器)方法。

python3.3后,pep380对生成器函数做了两处改动:

生成器可以返回一个值;以前,如果生成器中给return语句提供值,会抛出SyntaxError异常。

引入yield from 语法,使用它可以把复杂的生成器重构成小型的嵌套生成器,省去之前把生成器的工作委托给子生成器所需的大量模板代码。

协程生成器的基本行为

首先说明一下,协程有四个状态,可以使用inspect.getgeneratorstate(…)函数确定:

GEN_CREATED # 等待开始执行

GEN_RUNNING # 解释器正在执行(只有在多线程应用中才能看到这个状态)

GEN_SUSPENDED # 在yield表达式处暂停

GEN_CLOSED # 执行结束

#! -*- coding: utf-8 -*-

import inspect

#  协程使用生成器函数定义:定义体中有yield关键字。

def simple_coroutine():
    print('-> coroutine started')    # yield 在表达式中使用;如果协程只需要从客户那里接收数据,yield关键字右边不需要加表达式(yield默认返回None)
    x = yield
    print('-> coroutine received:', x)


my_coro = simple_coroutine()# 和创建生成器的方式一样,调用函数得到生成器对象。
print(inspect.getgeneratorstate(my_coro))
# 协程处于 GEN_CREATED (等待开始状态)my_coro.send(None)
# 首先要调用next()函数,因为生成器还没有启动,没有在yield语句处暂停,所以开始无法发送数据
# 发送 None 可以达到相同的效果 my_coro.send(None) 

next(my_coro)# 此时协程处于 GEN_SUSPENDED (在yield表达式处暂停)

print(inspect.getgeneratorstate(my_coro))
# 调用这个方法后,协程定义体中的yield表达式会计算出42;现在协程会恢复,一直运行到下一个yield表达式,或者终止。

my_coro.send(42)
print(inspect.getgeneratorstate(my_coro))

运行上述代码,输出结果如下

GEN_CREATED
-> coroutine started
GEN_SUSPENDED
-> coroutine received: 42# 这里,控制权流动到协程定义体的尾部,导致生成器像往常一样抛出StopIteration异常

Traceback (most recent call last):
  File "/Users/gs/coroutine.py", line 18, in <module> 
    my_coro.send(42)
StopIteration

send方法的参数会成为暂停yield表达式的值,所以,仅当协程处于暂停状态是才能调用send方法。
如果协程还未激活(GEN_CREATED 状态)要调用next(my_coro) 激活协程,也可以调用my_coro.send(None)

如果创建协程对象后立即把None之外的值发给它,会出现下述错误:

>>> my_coro = simple_coroutine()
>>> my_coro.send(123)

Traceback (most recent call last):
  File "/Users/gs/coroutine.py", line 14, in <module>
    my_coro.send(123)
TypeError: can't send non-None value to a just-started generator

仔细看错误消息

can’t send non-None value to a just-started generator

最先调用next(my_coro) 这一步通常称为”预激“(prime)协程—即,让协程向前执行到第一个yield表达式,准备好作为活跃的协程使用。

再看一个两个值得协程

def simple_coro2(a):
    print('-> coroutine started: a =', a)
    b = yield a
    print('-> Received: b =', b)
    c = yield a + b
    print('-> Received: c =', c)

my_coro2 = simple_coro2(14)
print(inspect.getgeneratorstate(my_coro2))# 这里inspect.getgeneratorstate(my_coro2) 

得到结果为 GEN_CREATED (协程未启动)

next(my_coro2)
# 向前执行到第一个yield 处 打印 “-> coroutine started: a = 14”
# 并且产生值 14 (yield a 执行 等待为b赋值)

print(inspect.getgeneratorstate(my_coro2))
# 这里inspect.getgeneratorstate(my_coro2) 得到结果为 GEN_SUSPENDED (协程处于暂停状态)

my_coro2.send(28)
# 向前执行到第二个yield 处 打印 “-> Received: b = 28”
# 并且产生值 a + b = 42(yield a + b 执行 得到结果42 等待为c赋值)

print(inspect.getgeneratorstate(my_coro2))
# 这里inspect.getgeneratorstate(my_coro2) 得到结果为 GEN_SUSPENDED (协程处于暂停状态)

my_coro2.send(99)
# 把数字99发送给暂停协程,计算yield 表达式,得到99,然后把那个数赋值给c 打印 “-> Received: c = 99”
# 协程终止,抛出StopIteration

运行上述代码,输出结果如下

GEN_CREATED
-> coroutine started: a = 14
GEN_SUSPENDED
-> Received: b = 28
-> Received: c = 99
Traceback (most recent call last):
  File "/Users/gs/coroutine.py", line 37, in <module>
    my_coro2.send(99)
StopIteration

simple_coro2 协程的执行过程分为3个阶段,如下图所示

调用next(my_coro2),打印第一个消息,然后执行yield a,产出数字14.

调用my_coro2.send(28),把28赋值给b,打印第二个消息,然后执行 yield a + b 产生数字42

调用my_coro2.send(99),把99赋值给c,然后打印第三个消息,协程终止。

使用装饰器预激协程
我们已经知道,协程如果不预激,不能使用send() 传入非None 数据。所以,调用my_coro.send(x)之前,一定要调用next(my_coro)。
为了简化,我们会使用装饰器预激协程。

from functools import wraps

def coroutinue(func):
    '''
    装饰器: 向前执行到第一个`yield`表达式,预激`func`
    :param func: func name
    :return: primer
    '''    @wraps(func)
    def primer(*args, **kwargs):
        # 把装饰器生成器函数替换成这里的primer函数;调用primer函数时,返回预激后的生成器。
        gen = func(*args, **kwargs)        # 调用被被装饰函数,获取生成器对象
        next(gen)  # 预激生成器
        return gen  # 返回生成器
    return primer

# 使用方法如下

@coroutinue
def simple_coro(a):
    a = yield

simple_coro(12)  # 已经预激
终止协程和异常处理
协程中,为处理的异常会向上冒泡,传递给next函数或send方法的调用方,未处理的异常会导致协程终止。

看下边这个例子

#! -*- coding: utf-8 -*-

from functools import wraps

def coroutinue(func):
    '''
    装饰器: 向前执行到第一个`yield`表达式,预激`func`
    :param func: func name
    :return: primer
    '''    
    @wraps(func)
    def primer(*args, **kwargs):
        # 把装饰器生成器函数替换成这里的primer函数;调用primer函数时,返回预激后的生成器。
        gen = func(*args, **kwargs)        # 调用被被装饰函数,获取生成器对象
        next(gen)  # 预激生成器
        return gen  # 返回生成器
    return primer

@coroutinue
def averager():
    # 使用协程求平均值
    total = 0.0
    count = 0
    average = None
    while True:
        term = yield average
        total += term
        count += 1
        average = total/count

coro_avg = averager()
print(coro_avg.send(40))
print(coro_avg.send(50))
print(coro_avg.send('123')) # 由于发送的不是数字,导致内部有异常抛出。

执行上述代码结果如下

40.0
45.0

Traceback (most recent call last):
  File "/Users/gs/coro_exception.py", line 37, in <module>
    print(coro_avg.send('123'))
  File "/Users/gs/coro_exception.py", line 30, in averager
    total += term
TypeError: unsupported operand type(s) for +=: 'float' and 'str'

出错的原因是发送给协程的’123’值不能加到total变量上。
出错后,如果再次调用 coro_avg.send(x) 方法 会抛出 StopIteration 异常。

由上边的例子我们可以知道,如果想让协程退出,可以发送给它一个特定的值。比如None和Ellipsis。(推荐使用Ellipsis,因为我们不太使用这个值)
从Python2.5 开始,我们可以在生成器上调用两个方法,显式的把异常发给协程。
这两个方法是throw和close。

generator.throw(exc_type[, exc_value[, traceback]])

这个方法使生成器在暂停的yield表达式处抛出指定的异常。如果生成器处理了抛出的异常,代码会向前执行到下一个yield表达式,而产出的值会成为调用throw方法得到的返回值。如果没有处理,则向上冒泡,直接抛出。

generator.close()

生成器在暂停的yield表达式处抛出GeneratorExit异常。
如果生成器没有处理这个异常或者抛出了StopIteration异常,调用方不会报错。如果收到GeneratorExit异常,生成器一定不能产出值,否则解释器会抛出RuntimeError异常。

示例: 使用close和throw方法控制协程。

import inspect

class DemoException(Exception):
    pass


@coroutinue
def exc_handling():
    print('-> coroutine started')    
    while True:        
        try:
            x = yield
        except DemoException:
            print('*** DemoException handled. Conginuing...')
        else:            # 如果没有异常显示接收到的值
            print('--> coroutine received: {!r}'.format(x))    
    raise RuntimeError('This line should never run.')  # 这一行永远不会执行

exc_coro = exc_handling()

exc_coro.send(11)
exc_coro.send(12)
exc_coro.send(13)
exc_coro.close()
print(inspect.getgeneratorstate(exc_coro))
raise RuntimeError('This line should never run.') 
#永远不会执行,因为只有未处理的异常才会终止循环,而一旦出现未处理的异常,协程会立即终止。

执行上述代码得到结果为:

-> coroutine started
--> coroutine received: 11
--> coroutine received: 12
--> coroutine received: 13
GEN_CLOSED    # 协程终止

上述代码,如果传入DemoException,协程不会中止,因为做了异常处理。

e

xc_coro = exc_handling()

exc_coro.send(11)
exc_coro.send(12)
exc_coro.send(13)
exc_coro.throw(DemoException) # 协程不会中止,但是如果传入的是未处理的异常,协程会终止
print(inspect.getgeneratorstate(exc_coro))
exc_coro.close()
print(inspect.getgeneratorstate(exc_coro))

## output-> coroutine started
--> coroutine received: 11
--> coroutine received: 12
--> coroutine received: 13
*** DemoException handled. Conginuing...
GEN_SUSPENDED
GEN_CLOSED

如果不管协程如何结束都想做些处理工作,要把协程定义体重的相关代码放入try/finally块中。

@coroutinue
def exc_handling():
    print('-> coroutine started')
    try:
        while True:
            try:
                x = yield
            except DemoException:
                print('*** DemoException handled. Conginuing...')
            else:                # 如果没有异常显示接收到的值
                print('--> coroutine received: {!r}'.format(x))
    finally:
        print('-> coroutine ending')

上述部分介绍了:

生成器作为协程使用时的行为和状态

使用装饰器预激协程

调用方如何使用生成器对象的 .throw(…)和.close() 方法控制协程

下一部分将介绍:

协程终止时如何返回值

yield新句法的用途和语义

协程(Coroutine)是一种用户态的轻量级线程,它可以在单个线程中实现多任务并发处理。Python中的协程通过生成器(generator)实现,使用yield语句来实现协程的暂停和恢复操作。 在Python 3.5之后,Python引入了async/await关键字,使得协程的使用更加方便和简洁。 下面是一个使用yield实现协程的示例: ```python def coroutine(): print("coroutine started") while True: value = yield print("Received value: ", value) c = coroutine() next(c) # 启动协程 c.send(1) # 发送值,并打印接收到的值 c.send(2) ``` 输出: ``` coroutine started Received value: 1 Received value: 2 ``` 在上面的代码中,使用yield语句实现了协程的暂停和恢复操作。在调用`c.send()`方法时,会将值发送给协程,并从yield语句处恢复协程的执行。协程会处理接收到的值,并在下一个yield语句处暂停,等待下一次发送。 除了使用yield语句来实现协程外,Python 3.5之后还可以使用async/await关键字来定义协程。使用async/await关键字定义的协程更加简洁和易于理解。下面是一个使用async/await关键字实现的协程示例: ```python async def coroutine(): print("coroutine started") while True: value = await asyncio.sleep(1) print("Received value: ", value) asyncio.run(coroutine()) ``` 在上面的代码中,使用async/await关键字定义了一个协程。使用`asyncio.sleep()`函数来实现协程的暂停操作,并在下一次事件循环时恢复协程的执行。使用`asyncio.run()`函数来运行协程。 总的来说,协程是一种非常有用的并发编程技术,可以在单个线程中实现高并发的处理。在Python中,可以使用生成器和async/await关键字来实现协程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值