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最大的特点是允许代码发生中断,并在调用nextsend时继续往下执行。

Python中使用生成器实现协程

协程是一种通过代码实现的模拟多线程并发的逻辑,其特点是使用一个线程实现了原本需要多个线程才能实现的功能;而且由于避免了多线程切换,提升了程序的性能,甚至去掉了多线程中必不可少的互斥锁。

协程最大的一个特点是用同步的方式写异步代码,提升了代码的可读性,并降低了维护成本。

协程与多线程的主要差别如下:

  1. 协程只有一个线程,多线程有多个线程
  2. 协程中任务(逻辑线程)的切换是在代码中主动进行的;线程的切换是操作系统进行的,时机不可预期
  3. 进程中可以创建的线程数量是有限的,数量多了之后产生的线程切换开销比较大;协程可以创建的任务数量主要受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('\r\n\r\n')
    raise tornado.gen.Return(buffer)

tcp_client_demo('1.1.1.1', 1111)
tornado.ioloop.IOLoop.current().start()

不过tornado中还是有很多地方需要写回调函数的,个人觉得这些地方实现得不是很优雅。

Python从3.5开始支持asyncawait关键字,从而在语言层面支持了协程。但是使用生成器实现协程的兼容性会更好。

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中nextsend的功能。

JavaScript中使用生成器实现协程

JavaScript天生是一个单线程的环境,一般不能使用阻塞的操作,传统的实现多采用异步回调(callback)方式。但是,这种方式容易导致层层嵌套,变成回调地狱(Callback Hell),阅读和调试都不是很方便。

后来出现了Promise,可以用优雅一些的方法编写异步代码,但是仍然不够优雅。于是出现了基于生成器Promise实现的co库,这个库目前只有200多行代码,可以将生成器函数变成Promise对象,并自动执行。它支持yield一个Promise对象,其效果与asyncawait(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秒,怀疑是执行误差所致。使用asyncawait也是如此,尚未找到具体原因。

如果只是不断调用gen_sleepnext函数,是不会进行sleep操作的。

使用asyncawait实现以上的功能,代码如下:

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 总结

从上面的例子可以看出,两者对生成器和协程的使用有很多相似之处,可以说是大同小异。在理解了语言的这些特性之后,编写协程代码会更加地轻松。

总的来说就是:语言都是相通的

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏QQ会员技术团队的专栏

JavaScript引擎分析

JavaScript引擎分析 一. JavaScript简介 JavaScript是一种动态类型的脚本语言;在1995年时,由Netscape公司的Brend...

2445
来自专栏Android先生

Java多线程-带你认识Java内存模型,内存分区,从原理剖析Volatile关键字

地址:https://juejin.im/post/59f8231a5188252946503294

1003
来自专栏月色的自留地

Python2中文处理纪要

1715
来自专栏欧阳大哥的轮子

深入解构iOS系统下的全局对象和初始化函数

事件源于接入了一个第三方库导致应用出现了大量的crash记录,很奇怪的是这么多的crash居然没有收到用户的反馈信息! 在这个过程中每个崩溃栈的信息都明确的指向...

2012
来自专栏Linyb极客之路

对象共享:Java并发环境中的烦心事

并发的意义在于多线程协作完成某项任务,而线程的协作就不可避免地需要共享数据。今天我们就来讨论下如何发布和共享类对象,使其可以被多个线程安全地访问。

1364
来自专栏韩伟的专栏

框架设计原则和规范(完)

祝大家圣诞节快乐!有事没事别出门,外面太!挤!了! 此文是《.NET:框架设计原则、规范》的读书笔记,本文内容较多,共分九章,今天推送最后一章。 1. 什么是好...

2874
来自专栏互联网杂技

堆,栈,内存泄露,内存溢出介绍

简单的可以理解为: heap(堆):是由malloc之类函数分配的空间所在地。地址是由低向高增长的。 stack(栈):是自动分配变量,以及函数调用的时候所使用...

4044
来自专栏开源优测

[快学Python3]基础知识

设置源文件编码 在默认情况下,Python3源码文件是以UTF-8编码进行保存的,所有的字符串都是unicode编码格式。 一般情况下,我们在源码文件第一行使用...

28513
来自专栏IT可乐

JVM 运行时的内存分配

  首先我们必须要知道的是 Java 是跨平台的。而它之所以跨平台就是因为 JVM 不是跨平台的。JVM 建立了 Java 程序和操作系统之间的桥梁,JVM 是...

2068
来自专栏阮一峰的网络日志

asm.js 和 Emscripten 入门教程

Web 技术突飞猛进,但是有一个领域一直无法突破 ---- 游戏。 游戏的性能要求非常高,一些大型游戏连 PC 跑起来都很吃力,更不要提在浏览器的沙盒模型里跑了...

3065

扫码关注云+社区

领取腾讯云代金券