Python 協程與 JavaScript 協程的對比
前言
以前沒怎么接觸前端,對 JavaScript 的異步操作不了解,現在有了點了解。一查發(fā)現 Python 和 JavaScript 的協程發(fā)展史簡直就是一毛一樣!
這里大致做下橫向對比和總結,便于對這兩個語言有興趣的新人理解和吸收。
共同訴求
- 隨著 cpu 多核化,都需要實現由于自身歷史原因(單線程環(huán)境)下的并發(fā)功能
- 簡化代碼,避免回調地獄,關鍵字支持
- 有效利用操作系統資源和硬件:協程相比線程,占用資源更少,上下文更快
什么是協程?
總結一句話,協程就是滿足下面條件的函數:
- 可以暫停執(zhí)行(暫停的表達式稱為暫停點)
- 可以從掛起點恢復(保留其原始參數和局部變量)
- 事件循環(huán)是異步編程的底層基石
混亂的歷史
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 庫"轉正" (之后的官方文檔就清晰了很多)
在主線發(fā)展過程中,也出現了很多支線的協程實現如 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(){}* 可以賦予函數執(zhí)行暫停/保存上下文/恢復執(zhí)行狀態(tài)的功能), 新關鍵詞 yield 使生成器函數暫停。
- ES7 引入 async函數/await語法糖,async 可以聲明一個異步函數(將 Generator 函數和自動執(zhí)行器,包裝在一個函數里),此函數需要返回一個 Promise 對象。await 可以等待一個 Promise 對象 resolve,并拿到結果
Promise 中也利用了回調函數。在 then 和 catch 方法中都傳入了一個回調函數,分別在 Promise 被滿足和被拒絕時執(zhí)行,這樣就就能讓它能夠被鏈接起來完成一系列任務。
總之就是把層層嵌套的 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() 等函數被封裝為一個 任務,該協程會被自動調度執(zhí)行
- Task 對象被用來在事件循環(huán)中運行協程。如果一個協程在等待一個 Future 對象,Task 對象會掛起該協程的執(zhí)行并等待該 Future 對象完成。當該 Future 對象 完成,被打包的協程將恢復執(zhí)行。
- 事件循環(huán)使用協同日程調度: 一個事件循環(huán)每次運行一個 Task 對象。而一個 Task 對象會等待一個 Future 對象完成,該事件循環(huán)會運行其他 Task、回調或執(zhí)行 IO 操作。
- asyncio.Task 從 Future 繼承了其除 Future.set_result() 和 Future.set_exception() 以外的所有 API。
未來對象(Future):
- Future 對象用來鏈接 底層回調式代碼 和高層異步/等待式代碼。
- 不用回調方法編寫異步代碼后,為了獲取異步調用的結果,引入一個 Future 未來對象。Future 封裝了與 loop 的交互行為,add_done_callback 方法向 epoll 注冊回調函數,當 result 屬性得到返回值后,會運行之前注冊的回調函數,向上傳遞給 coroutine。
幾種事件循環(huán)(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 繼續(xù)使用
Promise 本質是一個狀態(tài)機,用于表示一個異步操作的最終完成 (或失敗), 及其結果值。它有三個狀態(tài):
- pending: 初始狀態(tài),既不是成功,也不是失敗狀態(tài)。
- fulfilled: 意味著操作成功完成。
- rejected: 意味著操作失敗。
最終 Promise 會有兩種狀態(tài),一種成功,一種失敗,當 pending 變化的時候,Promise 對象會根據最終的狀態(tài)調用不同的處理函數。
async、await語法糖
async、await 是對 Generator 和 Promise 組合的封裝,使原先的異步代碼在形式上更接近同步代碼的寫法,并且對錯誤處理/條件分支/異常堆棧/調試等操作更友好。
js 異步執(zhí)行的運行機制
- 所有任務都在主線程上執(zhí)行,形成一個執(zhí)行棧。
- 主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
- 一旦"執(zhí)行棧"中的所有同步任務執(zhí)行完畢,系統就會讀取"任務隊列"。那些對應的異步任務,結束等待狀態(tài),進入執(zhí)行棧并開始執(zhí)行。
遇到同步任務直接執(zhí)行,遇到異步任務分類為宏任務(macro-task)和微任務(micro-task)。
當前執(zhí)行棧執(zhí)行完畢時會立刻先處理所有微任務隊列中的事件,然后再去宏任務隊列中取出一個事件。同一次事件循環(huán)中,微任務永遠在宏任務之前執(zhí)行。
例子
- 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 將任務劃分:
- 主線程循環(huán)從"任務隊列"中讀取事件
- 宏隊列(macro task)js 同步執(zhí)行的代碼塊,setTimeout、setInterval、XMLHttprequest、setImmediate、I/O、UI rendering等,本質是參與了事件循環(huán)的任務
- 微隊列(micro task)Promise、process.nextTick(node環(huán)境)、Object.observe, MutationObserver等,本質是直接在 Javascript 引擎中的執(zhí)行的沒有參與事件循環(huán)的任務
擴展閱讀 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 | 關鍵詞支持,一毛一樣 |
事件循環(huán) | asyncio 應用的核心。事件循環(huán)會運行異步任務和回調,執(zhí)行網絡 IO 操作,以及運行子進程。asyncio 庫支持的 API 較多,可控性高 | 基于瀏覽器環(huán)境基本是黑盒,外部基本無法控制,對任務有做優(yōu)先級分類,調度方式有區(qū)別 | 這里有很大區(qū)別,運行環(huán)境不同,對任務的調度先后不同,Python 可能和 Node.js 關于事件循環(huán)的可比性更高些,這里還需需要繼續(xù)學習 |
到這里就基本結束了,看完不知道你會有什么感想,如有錯誤還請不吝賜教。