1、前言
- 隨著 cpu 多核化,都需要實現由于自身歷史原因(單線程環境)下的并發功能
- 簡化代碼,避免回調地獄,關鍵字支持
- 有效利用操作系統資源和硬件:協程相比線程,占用資源更少,上下文更快
2、什么是協程?
總結一句話,協程就是滿足下面條件的函數:
- 可以暫停執行(暫停的表達式稱為暫停點)
- 可以從掛起點恢復(保留其原始參數和局部變量)
- 事件循環是異步編程的底層基石
3、混亂的歷史
3.1 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
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 " "" |
4、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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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 */ |
5、Python 協程成熟體
可等待對象可以在 await
語句中使用,可等待對象有三種主要類型:協程(coroutine
), 任務(task
) 和 Future
。
5.1 協程(coroutine)
-
協程函數:定義形式為
async def
的函數; - 協程對象:調用 協程函數 所返回的對象
-
舊式基于
generator
(生成器)的協程
5.2 任務(Task 對象)
-
任務 被用來“并行的”調度協程, 當一個協程通過
asyncio.create_task()
等函數被封裝為一個 任務,該協程會被自動調度執行 -
Task 對象被用來在事件循環中運行協程。如果一個協程在等待一個
Future
對象,Task 對象會掛起該協程的執行并等待該 Future 對象完成。當該Future
對象 完成,被打包的協程將恢復執行。 - 事件循環使用協同日程調度: 一個事件循環每次運行一個 Task 對象。而一個 Task 對象會等待一個 Future 對象完成,該事件循環會運行其他 Task、回調或執行 IO 操作。
-
asyncio.Task
從Future
繼承了其除Future.set_result()
和Future.set_exception()
以外的所有 API。
5.3 未來對象(Future)
Future
對象用來鏈接 底層回調式代碼 和高層異步/等待式代碼。
不用回調方法編寫異步代碼后,為了獲取異步調用的結果,引入一個 Future
未來對象。Future
封裝了與 loop
的交互行為,add_done_callback
方法向 epoll 注冊回調函數,當 result
屬性得到返回值后,會運行之前注冊的回調函數,向上傳遞給 coroutine
。
5.4幾種事件循環(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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()) |
6、JavaScript 協程成熟體
6.1Promise 繼續使用
Promise
本質是一個狀態機,用于表示一個異步操作的最終完成 (或失敗), 及其結果值。它有三個狀態:
-
pending:
初始狀態,既不是成功,也不是失敗狀態。 -
fulfilled:
意味著操作成功完成。 -
rejected
: 意味著操作失敗。
最終 Promise
會有兩種狀態,一種成功,一種失敗,當 pending
變化的時候,Promise 對象會根據最終的狀態調用不同的處理函數。
6.2 async、await語法糖
async
、await
是對 Generator
和 Promise
組合的封裝,使原先的異步代碼在形式上更接近同步代碼的寫法,并且對錯誤處理/條件分支/異常堆棧/調試等操作更友好。
6.3 js 異步執行的運行機制
- 所有任務都在主線程上執行,形成一個執行棧。
-
主線程之外,還存在一個"任務隊列"(
task queue
)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。 - 一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列"。那些對應的異步任務,結束等待狀態,進入執行棧并開始執行。
遇到同步任務直接執行,遇到異步任務分類為宏任務(macro-task
)和微任務(micro-task
)。
當前執行棧執行完畢時會立刻先處理所有微任務隊列中的事件,然后再去宏任務隊列中取出一個事件。同一次事件循環中,微任務永遠在宏任務之前執行。
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
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(); |
6.4 event loop 將任務劃分
主線程循環從"任務隊列"中讀取事件
宏隊列(macro task
)js 同步執行的代碼塊,setTimeout
、setInterval
、XMLHttprequest
、setImmediate
、I/O、UI rendering
等,本質是參與了事件循環的任務
微隊列(micro task)Promise
、process.nextTick
(node環境)、Object.observe
, MutationObserve
r等,本質是直接在 Javascript 引擎中的執行的沒有參與事件循環的任務
擴展閱讀 Node.js 中的 EventLoop (Javascript 運營機制詳解再淺談 Event Loop)
7、總結與對比
說明 | 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 協程與 JavaScript 協程的對比的文章就介紹到這了,更多相關Python 協程與 JavaScript 協程內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://developer.51cto.com/art/202109/682489.htm