Python協程與JavaScript協程的對比

從零開始的程式設計師生活發表於2021-05-12

前言

以前沒怎麼接觸前端對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關於事件迴圈的可比性更高些,這裡還需需要繼續學習

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

參考

相關文章