此文已由作者张耕源授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。

在最近发布的 Python 3.5 版本中,官方正式引入了 async/await关键字、在 asyncio [1] 标准库中实现了IO多路复用、原生协程(coroutine)与 事件循环(event loop),让人耳目一新,本文也尝试对 Python 3.5 新增加的原生协程 机制与asyncio标准库相关的内容做一个小结。

IO多路复用与协程的引入,可以极大的提高高负载下程序的IO性能表现。几年前,M$在 C# 中便已经通过引入 async/await 关键字实现了一套异步IO机制,成为业界模范, Python现在引入相同的编程范式也算博众家之长。

事实上,在官方实现原生协程机制前,在目前比较流行的 Python 2.X 版本中,由于 GIL [2] 的存在,Python 程序的多线程/多进程性能非常不理想,使得协程成为 Python 并发编程的最佳模型,大量的 Python 项目都开始通过使用第三方库实现的协程编写程序 (如 eventlet / gevent )、特别是网络编程相关的 Python 项目。不过限于 Python 2 语言实现的局限,协程的实现比较原始,众多第三方库的实现并不统一,并且通常都需要 使用一些特殊的编程技巧(monkey patch / green 标准库等手段)才能实现非阻塞IO等特 性来真正提高性能。

相比之下,直接官方提供标准库原生支持"async io"与协程的 Python 3.5 编写程序无疑 更加方便。

async/await

官方在 PEP-492 [3] 中定义了 async/await 关键字来使用协程。

声明一个协程非常简单,通过 async 实现:

async def foo():
    pass

在普通的函数声明前加上async关键字,这个函数就变成了一个协程。

需要注意的是,在 async def 定义的协程内,不能含有 yield 或 yield from 表达式,否则会报 SyntaxError 异常。

await表示等待另一个协程执行完成返回,必须在协程内才能使用。

下面是一个官方文档中提供的协程简单示例,非常直观的表示了 async/await、协程 与事件循环的执行过程:

import asyncio

async def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    await asyncio.sleep(1.0)    return x + y async def print_sum(x, y):
    result = await compute(x, y)
    print("%s + %s = %s" % (x, y, result)) loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))
loop.close()

从图中可以看到,asyncio 标准库自带的事件循环负责所有协程的调度。在首先执行 print_sum 协程时,内部使用await等待另外一个协程compute的返回结果,于是本地 协程被挂起去执行协程compute。而协程compute执行过程中调用了 await asyncio.sleep(1.0),于是本地协程挂起1秒、将程序控制权交回事件循环,1秒 后再恢复协程compute的执行直到整个stack执行结束。

而在大量协程并发执行的过程中,除了在协程中主动使用 await,当本地协程发生 IO等待、调用 asyncio.sleep()等方法时,程序的控制权也会在不同的协程间切换,从 而在 GIL 的限制下实现最大程度的并发执行,不会由于等待IO等原因导致程序阻塞,达到 较高的性能表现。

值得一提的是,asyncio 和类似 libev 一样的众多第三方类库实现的事件循环一样, 在不同平台上会使用不同的轮询机制,比如在Linux平台上使用 poll/epoll、BSD平台上 使用 kqueue、NT内核上会直接使用Proactor模式的完成端口。

yield from

如果有一直关注 Python 3 原生协程实现的同学,应该会知道其实它是靠生成器 (Generator)实现的。yield from 则是在 PEP-380 [4] 中新增加的生成器定义 关键字,与原生协程的实现密不可分。

原理上 yield from 基本等价于 await,只是 await 针对协程的实现做了更多具体 的处理与约定。

yield from 表达式的语义定义有很长的一串,要彻底搞明白它的实现,需要先学习在 PEP-342 [5] 中描述的增强型生成器(Enhanced Generators),但这里我们可以简单的把 它看作一个生成器语法糖:

for v in g:    yield v

加上在生成器内

return value

等价于

raise StopIteration(value)

这样实现以后,像这样的表达式

y = f(x)

我们就可以用 yield from 语法将 f(x) 改造成协程实现了

y = yield from g(x)

g 是 f 的生成器,只需要保证两者最终返回值一致,我们并不关心生成器 g 的中间 状态。

如果要详细了解这里的实现,建议还是阅读 PEP-380 与 PEP-342 原文,里面有专门的 章节专门描述这个问题。

context manager

PEP-492 中让人眼前一亮的一点是定义了协程的上下文管理器(context manager),新增 了 async with 语法,让我们可以将一个上下文作为协程处理,在进入(enter)和退出 (exit)一个 BLOCK 时做协程调度操作:

async with EXPR as VAR:
    BLOCK

与普通的 context manager 相比,async 版本的上下文管理器在内部方法前加了字母 "a"

class AsyncContextManager:
    async def __aenter__(self):
        await log('entering context')     async def __aexit__(self, exc_type, exc, tb):
        await log('exiting context')

一种典型的应用就是进入临界区

class Lock:
    async def __aenter__(self):
        await self.lock.lock()     async def __aexit__(self, exc_type, exc, tb):
        await self.lock.release() async with Lock():
    ...

类似的语法还有 async for

async for TARGET in ITER:
    BLOCK

这里不再赘述。

promise/future

promise 是一种最近在 nodejs 流行起来另外一种异步编程范式,在不同的地方可能也 被称作 future / deferred,但一般都指的是同一种类似的东西。promise 并没有 一个非常官方的标准,我了解的比较知名的promise标准规范有 Promises A+ [6]。 Python 从 Python 3.4 开始也提供了 asyncio.Future 实现类似的功能。

promise 的核心思想是为一个异步操作定义操作成功和失败的不通情况下的的回调 函数。在 nodejs 这类缺少官方 await 语法支持的语言中,能有效减轻 callback hell 问题,让代码更简洁,减少 raw callback 写法导致的缩进太多的 问题,并能方便的实现链式操作。

而在 Python 中,语言语法本身提供的功能已经足够丰富,没有 callback hell 这类 问题,Future 则可以更专注的让我们可以将各种异步操作以一种顺序的、更接近人类 逻辑思维与自然语言方式描述出来:

import asyncio@asyncio.coroutinedef slow_operation(future):
    yield from asyncio.sleep(1)
    future.set_result('Future is done!') loop = asyncio.get_event_loop()
future = asyncio.Future()
asyncio.ensure_future(slow_operation(future))
loop.run_until_complete(future)
print(future.result())
loop.close()

小结

在现在流行的编程语言中,异步、协程、事件循环被越来越多的关注与使用,Python 中的 asyncio.coroutine、Ruby 中的 Fiber、Node 中的 Promise、甚至像 Scala 这样的语言 中也有了 Future,更不用说 Golang 这种在这条路上一头走到底的编程语言。 await/Future 这样的异步编程方式被越来越多的普及与使用, 以后可能会像 if、for 一样成为编程语言不可缺少的一部分,而协程这种轻量级线程在 某些情境下可能也会越来越多的代替目前的多线程/多进程成为主流的并发编程方式。

美中不足的是,秉承 Guido 一向以来只挖坑不填坑的习惯,Python 3.5 实现新的 async/await 关键字后,并没有给出具体的现有第三方类库如何向新的 native 协程 实现迁移的方案,更不用说一些流行的 Python 2 第三方类库目前连 Python 3 都不 支持。距离我们真正能方便流畅地使用体验它可能还需要一段时间。

References

免费体验云安全(易盾)内容安全、验证码等服务

更多网易技术、产品、运营经验分享请点击

相关文章:
【推荐】 网易考拉规则引擎平台架构设计与实践
【推荐】 真屏实据丨数据大屏设计实战—揭秘企业级数据大屏设计过程
【推荐】 网易七鱼 Android 高性能日志写入方案

小议Python3的原生协程机制的更多相关文章

  1. Python3的原生协程(Async/Await)和Tornado异步非阻塞

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_113 我们知道在程序在执行 IO 密集型任务的时候,程序会因为等待 IO 而阻塞,而协程作为一种用户态的轻量级线程,可以帮我们解决 ...

  2. 运筹帷幄决胜千里,Python3.10原生协程asyncio工业级真实协程异步消费任务调度实践

    我们一直都相信这样一种说法:协程是比多线程更高效的一种并发工作方式,它完全由程序本身所控制,也就是在用户态执行,协程避免了像线程切换那样产生的上下文切换,在性能方面得到了很大的提升.毫无疑问,这是颠扑 ...

  3. Python 原生协程------asyncio

    协程 在python3.5以前,写成的实现都是通过生成器的yield from原理实现的, 这样实现的缺点是代码看起来会很乱,于是3.5版本之后python实现了原生的协程,并且引入了async和aw ...

  4. 协程,greenlet原生协程库, gevent库

    协程简介 协程(coroutine),又称为微线程,纤程,是一种用户级的轻量级线程.协程拥有自己的寄存器上下文和栈. 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来时,恢复之前保存的上下文 ...

  5. python3.x Day6 协程

    协程:#定义来自牛人alex博客协程,又称微线程,纤程.英文名Coroutine.一句话说明什么是线程:协程是一种用户态的轻量级线程.协程拥有自己的寄存器上下文和栈.协程调度切换时,将寄存器上下文和栈 ...

  6. python3 - 多线程和协程速率测试对比

    多线程和协程都属于IO密集型,我通过以下用例测试多线程和协程的实际速率对比. 实例:通过socket客户端以多线程并发模式请求不同服务器端(这里服务器端分2种写法:第一种服务器通过协程实现,第二种服务 ...

  7. 11.python3标准库--使用进程、线程和协程提供并发性

    ''' python提供了一些复杂的工具用于管理使用进程和线程的并发操作. 通过应用这些计数,使用这些模块并发地运行作业的各个部分,即便是一些相当简单的程序也可以更快的运行 subprocess提供了 ...

  8. Python3 协程相关 - 学习笔记

    什么是协程 协程的优势 Python3中的协程 生成器 yield/send yield + send(利用生成器实现协程) 协程的四个状态 协程终止 @asyncio.coroutine和yield ...

  9. (zt)Lua的多任务机制——协程(coroutine)

    原帖:http://blog.csdn.net/soloist/article/details/329381 并发是现实世界的本质特征,而聪明的计算机科学家用来模拟并发的技术手段便是多任务机制.大致上 ...

随机推荐

  1. js实现网页中的"运行代码"功能

    <!DOCTYPE html> <html> <head> <meta charset='utf8' /> <title>网页中的运行代码功 ...

  2. 远程服务器上的weblogic项目管理(三)常用指令及常见错误

    weblogic的管理流程已在前两节整理完毕,接下来汇总一下linux环境下的weblogic管理常用指令及常见错误: 常用指令: ./startWebLogic.sh 启动weblogic ./st ...

  3. 同源策略 , CORS

    一 . 同源策略 同源策略( Same origin policy ) 是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响,可以说Web是构建在同源 ...

  4. poj 1469 COURSES (二分图模板应用 【*模板】 )

    COURSES Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 18454   Accepted: 7275 Descript ...

  5. C++中输出 位宽和小数点后位数 的控制

    要用到这个头文件: <iomanip> setw(x) : 表示控制输出x的位宽 setprecision(x) :表示 控制输出小数点后 x 位 cout.precision(x): 表 ...

  6. ubuntu安装 LNMP+redis

    一.更新软件源 1.修改软件源为163的源 sudo vim /etc/apt/sources.list 替换源为163的源: deb http://mirrors.163.com/ubuntu/ i ...

  7. HDU 1850 Being a Good Boy in Spring Festival(博弈·Nim游戏)

    Being a Good Boy in Spring Festival Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32 ...

  8. C++ 精确计时类

    http://hi.baidu.com/ronyo/blog/item/ee7e71cf7d46c338f8dc61ad .html    在一些程序中经常要统计一个算法/函数花费的时间,每次都重新写 ...

  9. openfire build(2)

    InterceptorManager PluginManager openfire 插件的中servlet 在web-custom.xml 中的配置 url 一定要小写,访问时不区别大写小 否则404 ...

  10. 无言以队Alpha阶段项目复审

    小组的名字和链接 优点 缺点,bug报告 (至少140字) 最终名次 (无并列) 甜美女孩 http://www.cnblogs.com/serendipity-zeng/p/9937832.html ...