小议Python3的原生协程机制
此文已由作者张耕源授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
在最近发布的 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的原生协程机制的更多相关文章
- Python3的原生协程(Async/Await)和Tornado异步非阻塞
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_113 我们知道在程序在执行 IO 密集型任务的时候,程序会因为等待 IO 而阻塞,而协程作为一种用户态的轻量级线程,可以帮我们解决 ...
- 运筹帷幄决胜千里,Python3.10原生协程asyncio工业级真实协程异步消费任务调度实践
我们一直都相信这样一种说法:协程是比多线程更高效的一种并发工作方式,它完全由程序本身所控制,也就是在用户态执行,协程避免了像线程切换那样产生的上下文切换,在性能方面得到了很大的提升.毫无疑问,这是颠扑 ...
- Python 原生协程------asyncio
协程 在python3.5以前,写成的实现都是通过生成器的yield from原理实现的, 这样实现的缺点是代码看起来会很乱,于是3.5版本之后python实现了原生的协程,并且引入了async和aw ...
- 协程,greenlet原生协程库, gevent库
协程简介 协程(coroutine),又称为微线程,纤程,是一种用户级的轻量级线程.协程拥有自己的寄存器上下文和栈. 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来时,恢复之前保存的上下文 ...
- python3.x Day6 协程
协程:#定义来自牛人alex博客协程,又称微线程,纤程.英文名Coroutine.一句话说明什么是线程:协程是一种用户态的轻量级线程.协程拥有自己的寄存器上下文和栈.协程调度切换时,将寄存器上下文和栈 ...
- python3 - 多线程和协程速率测试对比
多线程和协程都属于IO密集型,我通过以下用例测试多线程和协程的实际速率对比. 实例:通过socket客户端以多线程并发模式请求不同服务器端(这里服务器端分2种写法:第一种服务器通过协程实现,第二种服务 ...
- 11.python3标准库--使用进程、线程和协程提供并发性
''' python提供了一些复杂的工具用于管理使用进程和线程的并发操作. 通过应用这些计数,使用这些模块并发地运行作业的各个部分,即便是一些相当简单的程序也可以更快的运行 subprocess提供了 ...
- Python3 协程相关 - 学习笔记
什么是协程 协程的优势 Python3中的协程 生成器 yield/send yield + send(利用生成器实现协程) 协程的四个状态 协程终止 @asyncio.coroutine和yield ...
- (zt)Lua的多任务机制——协程(coroutine)
原帖:http://blog.csdn.net/soloist/article/details/329381 并发是现实世界的本质特征,而聪明的计算机科学家用来模拟并发的技术手段便是多任务机制.大致上 ...
随机推荐
- c++标准库比较
1 GNU standard c++ library debian发行版中使用的c++标准库是GNU standard c++标准库. 2 Boost debian发行版中也是用了boost库,但是不 ...
- 我的Java开发学习之旅------>解惑Java进行三目运算时的自动类型转换
今天看到两个面试题,居然都做错了.通过这两个面试题,也加深对三目运算是的自动类型转换的理解. 题目1.以下代码输出结果是(). public class Test { public static vo ...
- 采集练习(十一) php 获得电视节目预告---数据来自电视猫
昨天写了个采集搜视网的电视节目预告,刚好今天有心情,想采下其他网站提供的节目预告,发现 电视猫wap版 的提供的节目预告也蛮好采(需要正则)....感谢移动互联网! 电视猫的 wap版地址是 htt ...
- python -- redis连接与使用
前面我们简单介绍了redis nosql数据库,现在我们在python里面来使用redis. 一.python连接redis 在python中,要操作redis,目前主要是通过一个python-red ...
- ansible2
一.ansible模块(yum.pip.service.conr.user.group) 你是否知道ansible一共有多少模块呢?可以用以下命令查看: [root@localhost ~]# ans ...
- hashMap的线程不安全
hashMap是非线程安全的,表现在两种情况下: 1 扩容: t1线程对map进行扩容,此时t2线程来读取数据,原本要读取位置为2的元素,扩容后此元素位置未必是2,则出现读取错误数据. 2 hash碰 ...
- CATTI二级口译训练
Vice chancellor, faculty members and dear students, It is my great pleasure and privilege to visit C ...
- vsftpd虚拟用户【公司系统部分享】
一,安装相关工具包 #yum -y install pam vsftpd db4 db4-utils -- pam 是用来提供身份验证的 -- vsftpd 是ftp服务的主程序 -- db4支持文件 ...
- oracle数据库-备份ORACLE为dmp类型数据
刘备,为自己后期脑子不灵光时可以找个可以翻阅的地方. 一.第一部分导出ORACLE数据 1.数据库地址及账号密码: 数据库地址:10.10.10.132账号密码:oracle/oracle 2.使用X ...
- window/body/img/iframe 的onload事件
在html页面中,只有body,img,iframe这一类标签具有onload事件. onload事件表示在当前元素载入完成后发生的事件.其中,window也有onload事件,但是跟body的是同一 ...