Python协程与JavaScript协程的对比

前言

以前没怎么接触前端对JavaScript 的异步操作不了解,现在有了点了解一查,发现 python 和 JavaScript 的协程发展史简直就是一毛一样!
这里大致做下横向对比和总结,便于对这两个语言有兴趣的新人理解和吸收.

共同诉求

  • 随着cpu多核化,都需要实现由于自身历史原因(单线程环境)下的并发功能
  • 简化代码,避免回调地狱,关键字支持
  • 有效利用操作系统资源和硬件:协程相比线程,占用资源更少,上下文更快

什么是协程

总结一句话, 协程就是满足下面条件的函数:

  • 可以暂停执行(暂停的表达式称为暂停点)
  • 可以从挂起点恢复(保留其原始参数和局部变量)
  • 事件循环是异步编程的底层基石

混乱的历史

Python协程的进化

  • Python2.2 中,第一次引入了生成器
  • Python2.5 中,yield 关键字被加入到语法中
  • Python3.4 时有了yield from(yield from约等于yield+异常处理+send), 并试验性引入的异步I/O框架 asyncio(PEP 3156)
  • Python3.5 中新增了async/await语法(PEP 492)
  • Python3.6 中asyncio库"转正" (之后的官方文档就清晰了很多)

在主线发展过程中也出现了很多支线的协程实现如Gevent

def foo():
    print("foo start")
    a = yield 1
    print("foo a", a)
    yield 2
    yield 3
    print("foo end")


gen = foo()
# print(gen.next())
# gen.send("a")
# print(gen.next())
# print(foo().next())
# print(foo().next())

# 在python3.x版本中,python2.x的g.next()函数已经更名为g.__next__(),使用next(g)也能达到相同效果。
# next()跟send()不同的地方是,next()只能以None作为参数传递,而send()可以传递yield的值.

print(next(gen))
print(gen.send("a"))
print(next(gen))
print(next(foo()))
print(next(foo()))

list(foo())

"""
foo start
1
foo a a
2
3
foo start
1
foo start
1
foo start
foo a None
foo end
"""

JavaScript协程的进化

  • 同步代码
  • 异步JavaScript: callback hell
  • ES6引入 Promise/a+, 生成器Generators(语法 function foo(){}* 可以赋予函数执行暂停/保存上下文/恢复执行状态的功能), 新关键词yield使生成器函数暂停.
  • ES7引入 async函数/await语法糖,async可以声明一个异步函数(将Generator函数和自动执行器,包装在一个函数里),此函数需要返回一个 Promise 对象。await 可以等待一个 Promise 对象 resolve,并拿到结果,

Promise中也利用了回调函数。在then和catch方法中都传入了一个回调函数,分别在Promise被满足和被拒绝时执行, 这样就就能让它能够被链接起来完成一系列任务。
总之就是把层层嵌套的 callback 变成 .then().then()...,从而使代码编写和阅读更直观

生成器Generator的底层实现机制是协程Coroutine。

function* foo() {
    console.log("foo start")
    a = yield 1;
    console.log("foo a", a)
    yield 2;
    yield 3;
    console.log("foo end")
}

const gen = foo();
console.log(gen.next().value); // 1
// gen.send("a") // http://www.voidcn.com/article/p-syzbwqht-bvv.html SpiderMonkey引擎支持 send 语法
console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
console.log(foo().next().value); // 1
console.log(foo().next().value); // 1

/*
foo start
1
foo a undefined
2
3
foo start
1
foo start
1
*/

Python协程成熟体

可等待对象可以在 await 语句中使用, 可等待对象有三种主要类型: 协程(coroutine), 任务(task) 和 Future.

协程(coroutine):

  • 协程函数: 定义形式为 async def 的函数;
  • 协程对象: 调用 协程函数 所返回的对象。
  • 旧式基于generator(生成器)的协程

任务(Task 对象):

  • 任务 被用来“并行的”调度协程, 当一个协程通过 asyncio.create_task() 等函数被封装为一个 任务,该协程会被自动调度执行
  • Task 对象被用来在事件循环中运行协程。如果一个协程在等待一个 Future 对象,Task 对象会挂起该协程的执行并等待该 Future 对象完成。当该 Future 对象 完成,被打包的协程将恢复执行。
  • 事件循环使用协同日程调度: 一个事件循环每次运行一个 Task 对象。而一个 Task 对象会等待一个 Future 对象完成,该事件循环会运行其他 Task、回调或执行 IO 操作。
  • asyncio.Task 从 Future 继承了其除 Future.set_result() 和 Future.set_exception() 以外的所有 API。

未来对象(Future):

  • Future 对象用来链接 底层回调式代码 和高层异步/等待式代码。
  • 不用回调方法编写异步代码后,为了获取异步调用的结果,引入一个 Future 未来对象。Future 封装了与 loop 的交互行为,add_done_callback 方法向 epoll 注册回调函数,当 result 属性得到返回值后,会运行之前注册的回调函数,向上传递给 coroutine。

几种事件循环(event loop):

  • libevent/libev: Gevent(greenlet+前期libevent,后期libev)使用的网络库,广泛应用;
  • tornado: tornado框架自己实现的IOLOOP;
  • picoev: meinheld(greenlet+picoev)使用的网络库,小巧轻量,相较于libevent在数据结构和事件检测模型上做了改进,所以速度更快。但从github看起来已经年久失修,用的人不多。
  • uvloop: Python3时代的新起之秀。Guido操刀打造了asyncio库,asyncio可以配置可插拔的event loop,但需要满足相关的API要求,uvloop继承自libuv,将一些低层的结构体和函数用Python对象包装。目前Sanic框架基于这个库

例子

import asyncio
import time


async def exec():
    await asyncio.sleep(2)
    print('exec')

# 这种会和同步效果一直
# async def go():
#     print(time.time())
#     c1 = exec()
#     c2 = exec()
#     print(c1, c2)
#     await c1
#     await c2
#     print(time.time())

# 正确用法
async def go():
    print(time.time())
    await asyncio.gather(exec(),exec()) # 加入协程组统一调度
    print(time.time())

if __name__ == "__main__":
    asyncio.run(go())

JavaScript 协程成熟体

Promise继续使用

Promise 本质是一个状态机,用于表示一个异步操作的最终完成 (或失败), 及其结果值。它有三个状态:

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。

最终 Promise 会有两种状态,一种成功,一种失败,当 pending 变化的时候,Promise 对象会根据最终的状态调用不同的处理函数。

async、await语法糖

async、await 是对 Generator 和 Promise 组合的封装, 使原先的异步代码在形式上更接近同步代码的写法,并且对错误处理/条件分支/异常堆栈/调试等操作更友好.

js异步执行的运行机制

  1. 所有任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。

遇到同步任务直接执行,遇到异步任务分类为宏任务(macro-task)和微任务(micro-task)。
当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

例子

var sleep = function (time) {
    console.log("sleep start")
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve();
        }, time);
    });
};

async function exec() {
    await sleep(2000);
    console.log("sleep end")
}

async function go() {
    console.log(Date.now())
    c1 = exec()
    console.log("-------1")
    c2 = exec()
    console.log(c1, c2)
    await c1;
    console.log("-------2")
    await c2;
    console.log(c1, c2)
    console.log(Date.now())
}

go();

event loop将任务划分:

  • 主线程循环从"任务队列"中读取事件
  • 宏队列(macro task)js同步执行的代码块,setTimeout、setInterval、XMLHttprequest、setImmediate、I/O、UI rendering等, 本质是参与了事件循环的任务.
  • 微队列(micro task)Promise、process.nextTick(node环境)、Object.observe, MutationObserver等,本质是直接在 Javascript 引擎中的执行的没有参与事件循环的任务.

扩展阅读 Node.js中的 EventLoop

总结与对比

说明 python JavaScript 点评
进程 单进程 单进程 一致
中断/恢复 yield ,yield from,next,send yield ,next 基本相同,但 JavaScript 对 send 没啥需求
未来对象(回调包装) Futures Promise 解决callback,思路相同
生成器 generator Generator 将yield封装为协程Coroutine,思路一样
成熟后关键词 async、await async、await 关键词支持,一毛一样
事件循环 asyncio 应用的核心。事件循环会运行异步任务和回调,执行网络 IO 操作,以及运行子进程。asyncio 库支持的 API 较多,可控性高 基于浏览器环境基本是黑盒,外部基本无法控制,对任务有做优先级分类,调度方式有区别 这里有很大区别,运行环境不同,对任务的调度先后不同, Python可能和Node.js关于事件循环的可比性更高些,这里还需需要继续学习

到这里就基本结束了,看完不知道你会有什么感想,如有错误还请不吝赐教.

参考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章