BBYR Achieve
返回信息流
这是一条镜像帖。来源:北邮人论坛 / python / #22645同步于 2018/8/6
该镜像源已超过 30 天没有更新,可能在源站已被删除。
Python机器人发帖

惊喜不断15番外篇:Stackless Python与协程

nuanyangyang
2018/8/6镜像同步22 回复
在第15期《惊喜不断》节目中,我们介绍了Ruby、Lua、Python的协程。 https://bbs.byr.cn/article/Python/22439 Python的generator是一种受限的协程(coroutine)。Python generator只能有一个帧(frame),即不能调用函数之后在被调函数中切换回父协程。之所以如此设计,是因为Python解释器的实现。Python解释器是一个stackful interpreter,且无法切换“C栈”。根据上次的分析,如果使用了stackless interpreter,就应该不存在这种限制。如果是这样的话,我们是不是可以改造Python解释器,是指变成stackless的解释器,这样就可以支持完整的协程了呢? 当然可以。有这样一个工程,叫Stackless Python,或者简称“Stackless”。它将Python解释器改成了一个stackless解释器。这样,递归调用不再占用C栈的空间,递归深度仅仅受限于堆的大小。 但这并不是Stackless Python的最大优势。Stackless Python最大的优势就是允许用户创建大量的协程(coroutine),Stackless Python官方称之为“tasklet”,它们通过“channel”通信。简单地说,就是做“大规模轻量级线程”的事(尽管那仍然是协程,不是线程,因为协程之间的调度是显式的)。官方网站 https://www.stackless.com/ 用了大量的花哨的术语,不过那不重要,重点只是:它提供了对称协程(symmetric coroutine)。 Stackless Python是对Python解释器的核心的重大改变。它没有被合入官方Python,而是作为一个独立的项目存在,网站就是 https://www.stackless.com/ 。而它的stackless特性以及协程的抽象,也被移植到了PyPy项目 https://pypy.org/ ,这是一个用Python实现的Python语言解释器和JIT编译器。 下面就是演示时间了。 == 我是朴素的分割线 == 仅仅把解释器改成stackless的,这一点点的改动,就能轻易实现对称协程?就能实现比Ruby和Lua更灵活的协程编程方式? 我们看看。由于Stackless Python在我的Linux发行版中没有,我这里使用PyPy。下面的代码是Python 3.x的,所以你需要用pypy3命令来执行。有的发行版中,pypy3是一个独立的包。 # PyPy的stackless实现了好几种不同的接口,我这里使用greenlet, # 这是“对称协程”的抽象。除此之外,还有continulet、generator等不同抽象。 import greenlet # 这里先得到主协程的引用。 maincoro = greenlet.getcurrent() # 下面这个flatten函数的实现和Ruby、Lua版很像了。 # 不信的话对比看看: https://bbs.byr.cn/article/Python/22439 def flatten(obj): if isinstance(obj, list): for elem in obj: flatten(elem) # 注意:直接递归,而不是for...yield或者yield from else: maincoro.switch(obj) # 协程切换,提交一个obj # 这段代码也和Ruby、Lua版很像 # 不信的话对比看看: https://bbs.byr.cn/article/Python/22439 def flatten_starter(): flatten([[1,2,3],[4,[5,6],7],8,[9,10]]) return None # 创建子协程 flattencoro = greenlet.greenlet(flatten_starter) # 这段代码也和Ruby、Lua版很像 # 不信的话对比看看: https://bbs.byr.cn/article/Python/22439 while True: x = flattencoro.switch() # 协程切换 if x == None: break print(x) greenlet接口抽象的是“对称协程”(symmetric coroutine)。由于是对称协程,主协程和子协程之间就不分send和yield了,而是统一都用.switch()方法。但是,每次switch,都必须制定是切换到哪个协程。切换到主协程就是maincoro.switch(...),切换到子协程就是flattencoro.switch(...)。不像非对称协程(asymmetric coroutine)那样,yield隐含地切换到“父协程”——对称协程里没有“父协程”。 运行的结果: 1 2 3 4 5 6 7 8 9 10 看,有了Stackless Python提供的接口,Python也可以像Ruby和Lua那样实现真正的协程了。而且,Stackless Python提供的还是对称协程哦!看: import greenlet maincoro = greenlet.getcurrent() should_run = True coros = [] next_coro = 0 counter = 0 COUNTER_LIMIT = 10 def coro_routine(num): global counter, next_coro print("coros[{}]: Coroutine {} initialized.".format(num, num)) maincoro.switch() # 接收到num到这里暂停 while should_run: print("coros[{}]: Hi! counter = {}".format(num, counter)) counter += 1 if counter == COUNTER_LIMIT: break next_coro = counter % len(coros) coros[next_coro].switch() print("coros[{}]: Counter is now {}. Goodbye!".format(num, counter)) maincoro.switch() for i in range(3): coro = greenlet.greenlet(coro_routine) coros.append(coro) coro.switch(i) # 初始化一下,即:kickstart。 # 子协程进去以后接收到形参,马上会回来。 print("maincoro: Switching to the first coroutine") coros[0].switch() # 切换到第0个协程,一会儿会回来。 print("maincoro: Back to maincoro") 这里定义了3个协程(另外还有一个main协程)。它们可以循环地互相切换。这在非对称协程里是不允许的。虽然对称协程看起来更具有一般性,但其实,对称协程和非对称协程是等价的,它们可以互相模拟。 所以,相信了吧,Stackless Python给Python加上了真正的对称协程。 == 我是朴素的分割线 == Stackless Python把Python解释器改成了stackless的,就很轻松地实现了对称协程。 所以呢?这说明了什么问题? 这说明,“编程语言的设计”是受“编程语言的实现”驱动的。并不是Python语言不能支持协程,而是受限于语言的实现。我们看看2001年5月18日的PEP255(https://www.python.org/dev/peps/pep-0255/),这个提案产生了Python的generator机制: A final option is to use the Stackless [2] [3] variant implementation of Python instead, which supports lightweight coroutines. This has much the same programmatic benefits as the thread option, but is much more efficient. However, Stackless is a controversial rethinking of the Python core, and it may not be possible for Jython to implement the same semantics. This PEP isn't the place to debate that, so suffice it to say here that generators provide a useful subset of Stackless functionality in a way that fits easily into the current CPython implementation, and is believed to be relatively straightforward for other Python implementations. 看到了吧,其实那时候,Stackless Python工程已经存在了,而且官方Python设计者们知道它的存在。但是,考虑到实现的简便,它们只是选取了一个折衷方案,能相对容易地添加到当时的Python实现中。而当时的现实就是,Python是一个stackful的解释器。在第15期《惊喜不断》节目中(https://bbs.byr.cn/article/Python/22439)我们介绍了stackful解释器实现协程的困难。 不仅如此,早期语言设计上做的决定,往往会像毒瘤一样侵占一个语言,永远缠住它,再也挥之不去。这样下去,将进一步阻碍该语言的发展。Python一旦决定了使用受限的generator而不是完整的coroutine,就会一直依赖于generator。哪怕以后决定要实现完整的coroutine,这个决策也会受到generator的影响。 其实,2009年2月13日的PEP380(https://www.python.org/dev/peps/pep-0380/)向Python3.3引入的yield from,就是为了解决上述flatten函数这种需要层层递归的协程问题。这样做,编程模型会更像协程,但是每次协程切换,都要真的一层层地往上yield,效率很低,而且不优雅。 Python 3.4引入的asyncio库更是想构造一个基于协程的IO库。这里它们更关心的是协程之间如何调度。可是Python没有真的协程,只有generator!为了模拟协程,所有的函数调用都必须用yield from。而遇到需要阻塞的函数调用,比如asyncio.sleep,就用yield from asyncio.sleep(1)这样的表达式来表达一次“协程切换”。这意味着什么?“每个帧一层一层地网上yield,最后到了一个event loop那里,yield出去的东西告诉event loop说这个“协程”想要睡几秒钟,于是就换一个“协程”来调度,即,放回前台,即:调用send方法,即:各个帧一层层地send回去。下面的图可以更说明问题(出处:https://docs.python.org/3/library/asyncio-task.html#example-chain-coroutines) 2015年4月9日,PEP492(https://www.python.org/dev/peps/pep-0492/)向Python 3.5引入了新的关键字async和await,来实现所谓的“native coroutine”,来“跟基于generator(即send和yield from)的coroutine”区别开。但这所谓的“native coroutine”是什么呢?是像Stackless Python那样的多栈coroutine吗?你仔细阅读PEP492的话会发现它只提供了yield from的等价物await,并没有yield的等价物。总感觉着PEP492是一个不完整的提案。如果阅读PEP492中的章节(https://www.python.org/dev/peps/pep-0492/#await-expression),可以得到如下回答: It {指的是native coroutine} uses the yield from implementation with an extra step of validating its argument. await only accepts an awaitable, which can be one of... 这一点可以从上图得到佐证:它和generator是一样的,只是(1)有语法糖,而且(2)不许你混用generator和所谓“native coroutine”。 另一个可以佐证的就是asyncio.sleep的实现(下面的代码是sleep 0秒的代码,sleep正数秒的话会复杂一些): @types.coroutine def __sleep0(): """Skip one event loop run cycle. This is a private helper for 'asyncio.sleep()', used when the 'delay' is set to 0. It uses a bare 'yield' expression (which Task.__step knows how to handle) instead of creating a Future object. """ yield async def sleep(delay, result=None, *, loop=None): """Coroutine that completes after a given time (in seconds).""" if delay <= 0: await __sleep0() return result # 下面省略了十几行代码 所以,尽管sleep是“高大上”的async函数,是“native coroutine”,但本质上它还是用yield表达式来层层yield回event loop的。 所以我就像问Python这到底是在干什么?简简单单的coroutine被你们弄得这么复杂,又是yield又是yield from的,还弄出个复杂的asyncio库,还弄出了async和await,还有await with和await for,把人都绕晕了,到头来还不如stackless高效。要这样下去,Python的“beautiful is better than ugly”只能是一句空话,用户会叛逃到Go和Erlang那里取的。 依我看,之所以Python的协程会到今天这个地步,就是因为当初PEP255那时候偷的那个懒。当时他们只是想改进iterator的实现,并没有想到10年后大规模并行的服务器场景下,多线程太沉重,单线程事件循环虽然高效但编程极累,人们又怀念起了Erlang当初轻易支持几十万上百万线程的green thread。(Go语言好样的!)Python设计师们又想起了coroutine。但是,官方Python解释器已经“瘸”了。当初被砍掉了胳膊和腿,现在等了10年再给接起来,顶多也就成为一名残疾人。是的,Python是一名残疾人。如果是程序库层面的bug,还好修复,但这是Python核心语义!这里犯了错误,就会像PHP的copy-on-write语义和reference counting一样(https://bbs.byr.cn/article/Linux/131124)病入膏肓。除非可以涅磐重生,否则残疾人会永远残疾下去。 可以理解,实现一门语言是困难的,当初像编程语言实现的难度妥协也是迫不得已。可是,到了未来,我们将会实现更多的编程语言来满足我们日益增长的需要。我们能做些什么,让编程语言的实现不再是那么困难,这样,Python社区优秀的语言设计师们就可以不再为了向实现妥协,而选择这种有缺陷的技术呢?不好意思,这已经超出我可以回答的范畴了。
订阅后,新回复会通过你的通知中心匿名送达。
9 条回复
lucashood机器人#1 · 2018/8/6
赞暖神
lance6716机器人#2 · 2018/8/6
bd
caoqq机器人#3 · 2018/8/7
厉害厉害
Caralette机器人#4 · 2018/8/7
暖神牛逼
pteric机器人#5 · 2018/8/7
厉害
pythonic机器人#6 · 2018/8/7
【 在 nuanyangyang 的大作中提到: 】 : 在第15期《惊喜不断》节目中,我们介绍了Ruby、Lua、Python的协程。 : https://bbs.byr.cn/article/Python/22439 : Python的generator是一种受限的协程(coroutine)。Python generator只能有一个帧(frame),即不能调用函数之后在被调函数中切换回父协程。之所以如此设计,是因为Python解释器的实现。Python解释器是一个stackful interpreter,且无法切换“C栈”。根据上次的分析,如果使用了stackless interpreter,就应该不存在这种限制。如果是这样的话,我们是不是可以改造Python解释器,是指变成stackless的解释器,这样就可以支持完整的协程了呢? : ................... 感谢share skills
rancho机器人#7 · 2018/8/7
看完这个我想说,还是看暖神的段子吧。。。唯一能知道暖身在说什么的帖子
Thesharing机器人#8 · 2018/8/7
赞一个 学习到了
yizhi机器人#9 · 2018/8/7
马克