Python 协程与 JavaScript 协程的对比

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

[[424288]]

 前言

以前没怎么接触前端,对 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。 

  1. def foo():  
  2.     print("foo start")  
  3.     a = yield 1  
  4.     print("foo a", a)  
  5.     yield 2  
  6.     yield 3  
  7.     print("foo end")  
  8. gen = foo()  
  9. # print(gen.next())  
  10. # gen.send("a")  
  11. # print(gen.next())  
  12. # print(foo().next())  
  13. # print(foo().next())  
  14. # 在python3.x版本中,python2.x的g.next()函数已经更名为g.__next__(),使用next(g)也能达到相同效果。  
  15. # next()跟send()不同的地方是,next()只能以None作为参数传递,而send()可以传递yield的值.  
  16. print(next(gen))  
  17. print(gen.send("a"))  
  18. print(next(gen))  
  19. print(next(foo()))  
  20. print(next(foo()))  
  21. list(foo())  
  22. """  
  23. foo start  
  24.  
  25. foo a a  
  26.  
  27.  
  28. foo start  
  29.  
  30. foo start  
  31.  
  32. foo start  
  33. foo a None  
  34. foo end  
  35. """ 

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。 

  1. function* foo() {  
  2.     console.log("foo start")  
  3.     a = yield 1;  
  4.     console.log("foo a", a)  
  5.     yield 2;  
  6.     yield 3;  
  7.     console.log("foo end")  
  8.  
  9. const gen = foo();  
  10. console.log(gen.next().value); // 1  
  11. // gen.send("a") // http://www.voidcn.com/article/p-syzbwqht-bvv.html SpiderMonkey引擎支持 send 语法  
  12. console.log(gen.next().value); // 2  
  13. console.log(gen.next().value); // 3 
  14. console.log(foo().next().value); // 1  
  15. console.log(foo().next().value); // 1  
  16. /*  
  17. foo start  
  18.  
  19. foo a undefined  
  20.  
  21.  
  22. foo start  
  23.  
  24. foo start  
  25.  
  26. */ 

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 框架基于这个库

例子 

  1. import asyncio  
  2. import time  
  3. async def exec():  
  4.     await asyncio.sleep(2)  
  5.     print('exec')  
  6. # 这种会和同步效果一直  
  7. # async def go():  
  8. #     print(time.time())  
  9. #     c1 = exec()  
  10. #     c2 = exec()  
  11. #     print(c1, c2)  
  12. #     await c1  
  13. #     await c2  
  14. #     print(time.time())  
  15. # 正确用法  
  16. async def go():  
  17.     print(time.time())  
  18.     await asyncio.gather(exec(),exec()) # 加入协程组统一调度 
  19.     print(time.time())  
  20. if __name__ == "__main__":  
  21.     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)。

当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行。

例子 

  1. var sleep = function (time) {  
  2.     console.log("sleep start")  
  3.     return new Promise(function (resolve, reject) {  
  4.         setTimeout(function () {  
  5.             resolve();  
  6.         }, time);  
  7.     });  
  8. };  
  9. async function exec() {  
  10.     await sleep(2000);  
  11.     console.log("sleep end")  
  12.  
  13. async function go() {  
  14.     console.log(Date.now())  
  15.     c1 = exec()  
  16.     console.log("-------1")  
  17.     c2 = exec()  
  18.     console.log(c1, c2)  
  19.     await c1;  
  20.     console.log("-------2")  
  21.     await c2;  
  22.     console.log(c1, c2)  
  23.     console.log(Date.now())  
  24. 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 (http://www.ruanyifeng.com/blog/2014/10/event-loop.html)

总结与对比

说明 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 关于事件循环的可比性更高些,这里还需需要继续学习

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

 

责任编辑:庞桂玉 来源: 菜鸟学Python
相关推荐

2023-11-17 11:36:59

协程纤程操作系统

2021-12-09 06:41:56

Python协程多并发

2024-02-05 09:06:25

Python协程Asyncio库

2017-09-22 16:08:16

Python协程编程

2023-10-12 09:46:00

并发模型线程

2022-04-19 20:39:03

协程多进程

2023-10-24 19:37:34

协程Java

2017-08-10 15:50:44

PHP协程阻塞

2024-06-27 07:56:49

2017-06-15 13:15:39

Python协程

2017-09-06 09:26:03

Python生成器协程

2021-04-25 09:36:20

Go协程线程

2022-09-06 20:30:48

协程Context主线程

2023-12-24 12:56:36

协程

2020-11-29 17:03:08

进程线程协程

2017-05-02 11:38:00

PHP协程实现过程

2023-08-08 07:18:17

协程管道函数

2016-10-28 17:39:47

phpgolangcoroutine

2023-11-04 20:00:02

C++20协程

2024-12-03 15:15:22

点赞
收藏

51CTO技术栈公众号