玩转 Python 3.5 的 await/async


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

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

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


1

2

3

4

5

6

7

8

9

10

11


def coro1():

    print("C1: Start")

    print("C1: Stop")

def coro2():

    print("C2: Start")

    print("C2: a")

    print("C2: b")

    print("C2: c")

    print("C2: Stop")

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


1

2


coro1()

coro2()

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


1

2

3

4

5

6

7


C1: Start

C1: Stop

C2: Start

C2: a

C2: b

C2: c

C2: Stop

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


1

2

3

4

5

6

7

8

9

10

11


async def coro1():

    print("C1: Start")

    print("C1: Stop")

async def coro2():

    print("C2: Start")

    print("C2: a")

    print("C2: b")

    print("C2: c")

    print("C2: Stop")


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


1

2

3


c1 = coro1()

c2 = coro2()

print(c1, c2)

输出:


1

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

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

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


1

await c1

不过,你肯定会失望了。await 表达式只有在本地协程函数里才是有效的。你必须这样做:


1

2


async def main():

    await c1

接下来问题来了,main 函数又是如何开始执行的呢?

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


1

c1.send(None)

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


1

2

3

4

5

6


C1: Start

C1: Stop

Traceback (most recent call last):

  File "test3.py", line 16in

    c1.send(None)

StopIteration

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


1

2

3

4

5

6

7

8


try:

    c1.send(None)

except StopIteration:

    pass

try:

    c2.send(None)

except StopIteration:

    pass


现在我们得到了全部的输出,不过有点让人失望的是这跟最初的输出结果没有啥区别。因此我们增加了不少代码,不过还没有做到交替执行。协程与线程相似的地方是多个线程之间也可以交替执行,不过与线程不同之处在于协程之间的切换是显式的,而线程是隐式的(大多数情况下是更好的方式)。所以我们需要加入显式切换的代码。

通常生成器的 send 方法会一直运行,直到通过 yield 关键字放弃执行,也许你认为我们的 coro1 可以改成这个样子:


1

2

3

4


async def coro1():

    print("C1: Start")

    yield

    print("C1: Stop")

但是我们不能在协程里使用 yield。作为替换,我们可以使用新的 await 表达式来暂停协程的执行,直到 awaitable 执行结束。于是我们需要的代码类似于 await _something_;问题是这里 _something_ 是什么呢?我们必须 await 某个东西,而不是空!这个 PEP 解释了什么是可以 await 的(awaitable)。其中一种是另一个本地协程,不过这个对我们了解底层细节没有啥帮助。另一种是通过特定 CPython API 定义的对象,不过我们暂时还不打算引入扩展模块,而只限于使用纯 Python。除此之外,还剩下两种选择:基于生成器的协程对象,或者一个特殊的类似 Future 的对象。


接下来,我们会选择基于生成器的协程对象。基本上一个 Python 的生成器(例如:某个有yield表达式的函数)可以通过 types.coroutine 装饰被标记成一个协程。所以,这是一个最简单的例子:


1

2

3


@types.coroutine

def switch():

    yield

这定义了一个基于生成器的协程函数。要得到基于生成器的协程对象,只需要执行这个函数。我们可以把我们的 coro1 协程修改成下面这样:


1

2

3

4


async def coro1():

    print("C1: Start")

    await switch()

    print("C1: Stop")

通过上面的修改,我们期望 coro1 和 coro2 可以交错执行。到目前为止,输出是这样的:


1

2

3

4

5

6


C1: Start

C2: Start

C2: a

C2: b

C2: c

C2: Stop

我没看到正如期望的,在第一条打印语句之后,coro1 停止执行,coro2 接着执行。实际上,我们可以通过下面的代码查看协程对象是如何暂停执行的:


1

print("c1 suspended at: {}:{}".format(c1.gi_frame.f_code.co_filename, c1.gi_frame.f_lineno))

这可以打印 await 表达式所在的行。(注意:打印的是最外层的 await,所以这里只是起示例作用,通常情况下用处不大)。


现在的问题是,如何让 coro1 继续执行完呢?我们可以再调用一次 send,代码如下:


1

2

3

4

5

6

7

8

9

10

11

12


try:

    c1.send(None)

except StopIteration:

    pass

try:

    c2.send(None)

except StopIteration:

    pass

try:

    c1.send(None)

except StopIteration:

    pass

得到的输出跟预期一样:


1

2

3

4

5

6

7


C1: Start

C2: Start

C2: a

C2: b

C2: c

C2: Stop

C1: Stop

目前,我们通过为不同的协程显式调用 send来让它们都执行结束。通常情况下这种方式不是很好。我们希望的是有一个函数来控制所有的协程的运行,直到全部协程都执行完成。换句话说,我们期望连续不断的调用 send,驱动不同的协程去执行,直到send抛出 StopIteration 异常。

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


1

2

3

4

5

6

7

8

9

10


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 的代码去掉,代码如下:


1

2

3


c1 = coro1()

c2 = coro2()

run([c1, c2])

综上所述,在 Python 3.5,我们现在可以通过新的 await 和 async 功能很轻松的执行协程。本文的相关代码可以在github 上找到。

如果想深入体验LINUX系统的新手,也可以先下载一个方德Linux软件中心试用一下。
免费下载地址:http://www.nfs-cloud.cn:81/appCenter/open/softcenter

时间: 2024-10-12 18:38:42

玩转 Python 3.5 的 await/async的相关文章

.net core 入坑经验 - 1、await async

已经有些日子没学习新知识了,心血来潮想试试core有多大变化和跨平台运行 所以现在就开始捣鼓,然而由于是从.net 4.0直接"跃升"到.net core 以及 asp.net mvc core..发现变化真是太大了. 准备记录一系列的遇到的问题和变化,方便以后出问题快速查阅吧. 首先我要实现一个获取网页HTML的方法,问题来了. 我需要使用HttpWebRequest对象进行获取字符串流,而在获取的过程中发现该类已经不存在以前的GetRequestStream()方法了,而变为了Ge

Silverlight项目笔记1:UI控件与布局、MVVM、数据绑定、await/async、Linq查询、WCF RIA Services、序列化、委托与事件

最近从技术支持转到开发岗,做Silverlight部分的开发,用的Prism+MVVM,框架由同事搭好,目前做的主要是功能实现,用到了一些东西,侧重于如何使用,总结如下:   1.UI控件与布局     2.MVVM     3.数据绑定     4.await/async     5.Linq查询     6.WCF RIA Services     7.序列化     8.委托与事件 1.UI控件与布局 常用的主要是Grid.StackPanel.Border,其中最常用的的是Grid,是一

玩蛇(Python)笔记之基础Part3

玩蛇(Python)笔记之基础Part1 一.集合 1.set 无序,不重复序列 {}创建,直接写元素 2.set功能 __init__()构造方法,,使用强制转换就会调用此方法 1 set1 = {'year', 'jiujiujiu'} 2 print(type(set1)) 3 # 创建集合 4 s = set() # 创建空集合 5 li = [11, 22, 11, 22] 6 s = set(li) set 3.集合的基本操作 1 # 操作集合 2 s1 = set() 3 s1.a

玩转Python让人讨厌的编码问题

Python的编码问题基本是每个新手都会遇到的坎,但只要完全掌握了就跳过了这个坑,万变不离其中,这不最近我也遇到了这个问题,来一起看看吧. 事情的起因是review同事做的一个上传功能,看下面一段代码,self.fp是上传的文件句柄 fpdata = [line.strip().decode('gbk').encode('utf-8').decode('utf-8') for line in self.fp] data = [''.join(['(', self.game, ',', ','.j

玩蛇(Python)笔记之基础Part2

玩蛇(Python)笔记之基础Part2 一.列表 1.列表 别的语言叫数组 python牛逼非要取个不一样的名字 1 age = 23 2 name = ["biubiubiu", "jiujiujiu", 22, age] 3 # namecopy = name 4 # namecopy.pop() 5 print(name) 6 # print(namecopy) List 2.列表取值 正常index 从零开始,,取倒数加负号 倒数第一就是[-1] 3.列表

从零单排之玩转Python安全编程(II)

转自:http://www.secpulse.com/archives/35893.html 都说Python大法好,作为一名合格的安全从业人员,不会几门脚本语言都不好意思说自己是从事安全行业的. 而Python是最容易入门且使用最顺手的脚本语言,为了不引起程序员世界大战,我们不说Python是世界上最好的语言,没有之一. 这是<从零单排之玩转Python安全编程>的第二篇.第一篇参见安全脉搏<从零单排之玩转Python安全编程(I)> 本教程继续展示一些基本的Python脚本概念

玩转python之字符串逐个字符或逐词反转

众所周知,python中的字符串是无法改变的,反转一个字符串自然要创建一个拷贝:嘴简单的方法,当然是步长为“-1”的切片: result = astring[::-1] 如果要是按单词来反转,需要三步完成:字符串--->单词列表.反转列表.单词列表--->字符串: 1 result = astring.split() 2 result.reverse() 3 result = ' '.join(result) 如果喜欢简练和紧凑的一行代码,可以这样做:result = ' '.join(ast

多线程-Task、await/async

Task创建无返回值 Task是.netframwork3.0重新分装的多线程类.原因以前的多线程(thread threadpool)不好用.(.net framwork也是的发展的,现在的EF,刚开始是一个edmx文件,现在的code first,ef轻量级.但是其他有的技术也是死掉了) Task具有线程执行的可控性,返回值,代码书写简单,性能好等特点. Task创建主要有三种方式 1.Task参数 Task t = new Task(() => { for (int i = 0; i <

5分种让你了解javascript异步编程的前世今生,从onclick到await/async

javascript与异步编程 为了避免资源管理等复杂性的问题,javascript被设计为单线程的语言,即使有了html5 worker,也不能直接访问dom. javascript 设计之初是为浏览器设计的GUI编程语言,GUI编程的特性之一是保证UI线程一定不能阻塞,否则体验不佳,甚至界面卡死. 一般安卓开发,会有一个界面线程,一个后台线程,保证界面的流畅.由于javascript是单线程,所以采用异步非阻塞的编程模式,javascript的绝大多数api都是异步api. 本文是本人的一个