Python和JavaScript中的生成器与协程
0x00 前言
Python和JavaScript中都有生成器
(Generator
)和协程
(coroutine
)的概念。本文通过分析两者在这两种语言上的使用案例,来对比它们的差异。
0x01 Python中的生成器
Python中的生成器简介
使用过Python的同学对生成器的概念应该是很熟悉的,一个经典的例子是使用它生成斐波拉契数列。
def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
输出结果如下:
>>> for n in fab(5):
... print n
...
1
1
2
3
5
在Python中,使用了yield
的函数不再是普通函数,而是一个生成器函数,执行它返回的是一个生成器对象,可以进行迭代,可以调用next
函数获取下一个值。
>>> fab
<function fab at 0x0225E4F0>
>>> fab(5)
<generator object fab at 0x0217BE18>
>>> x=fab(5)
>>> x.next()
1
>>> x.next()
1
>>> x.next()
2
>>> x.next()
3
>>> x.next()
5
yield
也支持使用send
方法进行参数传递。
def gen_test():
n = 1
while True:
n = yield n
print n
g = gen_test()
n = g.next()
for i in range(5):
n = g.send(n * 2)
输出结果为:
2
4
8
16
32
创建生成器函数后,需要先调用一次next
函数,否则程序会报以下错误:
TypeError: can't send non-None value to a just-started generator
yield
最大的特点是允许代码发生中断,并在调用next
或send
时继续往下执行。
Python中使用生成器实现协程
协程是一种通过代码实现的模拟多线程并发的逻辑,其特点是使用一个线程实现了原本需要多个线程才能实现的功能;而且由于避免了多线程切换,提升了程序的性能,甚至去掉了多线程中必不可少的互斥锁。
协程最大的一个特点是用同步的方式写异步代码,提升了代码的可读性,并降低了维护成本。
协程与多线程的主要差别如下:
- 协程只有一个线程,多线程有多个线程
- 协程中任务(逻辑线程)的切换是在代码中主动进行的;线程的切换是操作系统进行的,时机不可预期
- 进程中可以创建的线程数量是有限的,数量多了之后产生的线程切换开销比较大;协程可以创建的任务数量主要受CPU占用率、文件句柄数量等限制
由于Python中GIL
的存在,多线程实际上并无法利用到多核CPU的优势。这种情况下使用协程 + 多进程
无疑是最优实现方案。
yield
天生的特性,为实现协程提供了极大的便利。
Python中使用生成器实现协程的典型库是:tornado
。即便是自己实现也不是很复杂,基本原理就是维护一个事件队列,保存生成器对象,不断取出队列前面的生成器对象,去调用send
方法,进行参数传递,从而维护了函数调用链。
下面是使用tornado
的一个例子:
import tornado.gen
import tornado.ioloop
import tornado.tcpclient
@tornado.gen.coroutine
def tcp_client_demo(addr, port):
tcp_client = tornado.tcpclient.TCPClient()
stream = yield tcp_client.connect(addr, port, timeout=30)
stream.write('send data')
buffer = yield stream.read_until('rnrn')
raise tornado.gen.Return(buffer)
tcp_client_demo('1.1.1.1', 1111)
tornado.ioloop.IOLoop.current().start()
不过tornado
中还是有很多地方需要写回调函数的,个人觉得这些地方实现得不是很优雅。
Python从3.5
开始支持async
和await
关键字,从而在语言层面支持了协程。但是使用生成器实现协程的兼容性会更好。
0x02 JavaScript中的生成器
JavaScript中的生成器简介
JavaScript中可以使用function*
创建生成器函数,这是在ES6
规范中提出来的,Chrome从版本39
才开始支持这一特性。
使用JavaScript生成斐波拉契数列的代码如下:
function* fab(max) {
var [n, a, b] = [0, 0, 1];
while(n < max) {
yield b;
[a, b] = [b, a + b];
n++;
}
}
执行结果如下:
> x=fab(5)
fab {[[GeneratorStatus]]: "suspended"}
> x.next()
Object {value: 1, done: false}
> x.next()
Object {value: 1, done: false}
> x.next()
Object {value: 2, done: false}
> x.next()
Object {value: 3, done: false}
> x.next()
Object {value: 5, done: false}
可以看出,使用方法与Python中是基本一致的,不过,JavaScript中并没有send
方法,但是next
是可以传参的,相当于结合了Python中next
和send
的功能。
JavaScript中使用生成器实现协程
JavaScript天生是一个单线程的环境,一般不能使用阻塞的操作,传统的实现多采用异步回调(callback
)方式。但是,这种方式容易导致层层嵌套,变成回调地狱(Callback Hell
),阅读和调试都不是很方便。
后来出现了Promise
,可以用优雅一些的方法编写异步代码,但是仍然不够优雅。于是出现了基于生成器
和Promise
实现的co
库,这个库目前只有200多行代码,可以将生成器函数变成Promise对象,并自动执行。它支持yield
一个Promise
对象,其效果与async
和await
(Chrome 55
开始支持)相似。
co
代码链接为:https://github.com/tj/co/blob/master/index.js。
关于co
的具体介绍可以参考这篇文章。
以下是使用co
的一个例子:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function* gen_sleep() {
console.log(new Date());
yield sleep(2000);
console.log(new Date());
}
co(gen_sleep);
执行结果如下:
Wed Jul 18 2018 14:39:44 GMT+0800 (中国标准时间)
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
VM514:8 Wed Jul 18 2018 14:39:47 GMT+0800 (中国标准时间)
这里两次打印时间差了3秒
,怀疑是执行误差所致。使用async
和await
也是如此,尚未找到具体原因。
如果只是不断调用gen_sleep
的next
函数,是不会进行sleep
操作的。
使用async
和await
实现以上的功能,代码如下:
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function async_sleep() {
console.log(new Date());
await sleep(2000);
console.log(new Date());
}
async_sleep();
可以看出,这两种方式都可以实现协程的效果,但是后者是语言官方支持,应该会成为主流。
0x03 总结
从上面的例子可以看出,两者对生成器和协程的使用有很多相似之处,可以说是大同小异。在理解了语言的这些特性之后,编写协程代码会更加地轻松。
总的来说就是:语言都是相通的
。
- BZOJ 3038: 上帝造题的七分钟2【线段树区间开方问题】
- BZOJ 3211: 花神游历各国【线段树区间开方问题】
- WP、Win10开发或者WPF开发时绘制自定义窗体~例如:一个手机
- 【Java学习笔记之八】JavaBean中布尔类型使用注意事项
- BZOJ 1597: [Usaco2008 Mar]土地购买【斜率优化+凸包维护】
- BZOJ 1046: [HAOI2007]上升序列【贪心+二分状态+dp+递归】
- 【Java学习笔记之九】java二维数组及其多维数组的内存应用拓展延伸
- BZOJ 1293: [SCOI2009]生日礼物【单调队列】
- Javascript缓存投毒学习与实战
- 【Java学习笔记之十】Java中循环语句foreach使用总结及foreach写法失效的问题
- Codeforces 839B Game of the Rows【贪心】
- Codeforces 839A Arya and Bran【暴力】
- 【Java学习笔记之十一】Java中常用的8大排序算法详解总结
- 浅谈zip格式处理逻辑漏洞
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Python 技术篇-将项目打包成whl文件,whl包的制作方法
- PowerBI 超级粘性用户计算 - 原理与实现
- Chrome 技术篇-常用web调试手法:清除缓存并硬性重新加载
- 数据库之索引模块
- Python 爬虫篇-爬取web页面所有可用的链接实战演示,展示网页里所有可跳转的链接地址
- Python爬虫,微信公众号话题标签内容采集打印PDF输出
- Windows 技术篇-设置dns提升网速,刷新dns缓存
- 搭建高可用的Replication集群归档大量的冷数据
- Python 技术篇-文件操控:文件的移动和复制
- Python发邮件脚本,Python调用163邮箱SMTP服务实现邮件群发
- 为PXC集群引入Mycat并构建完整的高可用集群架构
- Python3 字典
- 安装Percona Server数据库(in CentOS 8)
- Python 基础篇-正斜杠("/")和反斜杠("")的用法
- 在CentOS8下搭建PXC集群