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關於事件循環的可比性更高些,這裏還需需要繼續學習

到這裏就基本結束了,看完不知道你會有什麼感想,如有錯誤還請不吝賜教.

參考

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