大家好,我是小雨小雨,致力於分享有趣的、實用的技術文章。
內容分為翻譯和原創,如果有問題,歡迎隨時評論或私信,希望和大家一起進步。
分享不易,希望能夠得到大家的支持和關注。
什么是協程
協程是在單核CPU場景中發展出來的概念,是非搶占的的多任務編程組件,並提供了掛起和恢復的執行接口。通過掛起和恢復多個任務,實現在單個CPU上交叉處理多個任務的並發功能。
有一個更具象的說法,如果大家看過阮一峰的進程與線程的解釋,那么對於協程,我們可以理解為是工人的最小組成部分。
就好像人類同時做很多事情其實也是一種並發的現象,並沒有真正的一心二用,只是很快的切換工作焦點。和協程異曲同工。
任務調度
線程是什么?要理解這個概念,需要先了解一下操作系統的一些相關概念。大部分操作系統(如Windows、Linux)的任務調度是采用時間片輪轉的搶占式調度方式。
在一個進程中,當一個線程任務執行幾毫秒后,會由操作系統的內核(負責管理各個任務)進行調度,通過硬件的計數器中斷處理器,讓該線程強制暫停並將該線程的寄存器放入內存中,通過查看線程列表決定接下來執行哪一個線程,並從內存中恢復該線程的寄存器,最后恢復該線程的執行,從而去執行下一個任務。
上述過程中,任務執行的那一小段時間叫做時間片,任務正在執行時的狀態叫運行狀態,被暫停的線程任務狀態叫做就緒狀態,意為等待下一個屬於它的時間片的到來。
這種方式保證了每個線程輪流執行,由於CPU的執行效率非常高,時間片非常短,在各個任務之間快速地切換,給人的感覺就是多個任務在“同時進行”,這也就是我們所說的並發(別覺得並發有多高深,它的實現很復雜,但它的概念很簡單,就是一句話:多個任務同時執行)。多任務運行過程的示意圖如下:
為什么引入協程
這里就得比較下線程和協程了。
線程可以讓開發者們充分用CPU多核計算資源,但是也引入了一些問題:
- 鎖競爭
如果多個線程在某個鎖上發生競爭,將導致多個線程無法充分的並發執行,此外過度競爭還會導致線程頻繁的發生上下文切換,這個鎖將成為系統性能的瓶頸
- 線程不能太多
超過CPU核數后,多余的線程只能等待時機去搶占CPU資源。在高並發場景,如雲計算,會成為瓶頸。
- 線程切換成本高,並且創造過多線程會導致OOM(堆空間內存溢出)
而協程就在一定程度上解決了部分問題:
- 不使用鎖
協程屬於線程內,即單核上的並發,每個人物都可以看做原子任務,不需要鎖介入。
- 海量協程
協程每個任務站的占用空間小,一個進程內包含n個線程,線程又可以包含n個協程,所以理論上可創造海量的協程
- 無需切換
不像進程和線程,協程由程序控制(也就是用戶),不需要系統切換,成本低
給我的感覺就是在線程上有分了一層子集,是不是有點類似動態規划划分子問題的感覺?直到這的大問題變成一個原子操作,不可拆分。那應該就是最簡單的任務,也就是我們想要的。
javascript與協程
我們知道v8執行js代碼是單線程的,通過上面的介紹,協程是非常適合於單線程的,可以在單線程的不同執行棧中來回切換,並且消耗要更小,性能要更好。
為了實現generators和協程es6引入了yield關鍵字,也就是產出或者暫停的意思,可以隨時暫停正在執行的函數,並保存當前函數的上下文環境。
不過由於不能指定讓步的協程,只能讓步給生成器(迭代器)的調用者,所以也稱為非對稱協程。
現在市面上主要有兩種協程的實現,一種是偽·協程,一種是真·協程。
- 偽·協程
其實還是利用的回調函數,比如co,對js原有的事件循環沒有影響
- 真·協程
比較著名的是node-fibers,但是如果阻塞掉了當前執行的協程,是會阻塞掉主線程的,也就是說,及時加入了協程,js還是單線的,也是時間分片的概念: 同一時間只有一個協程在運行,在協程掛起和執行期間,v8將當前環境保存,然后用對應協程的棧來填充主執行棧。
換句話說,只有所有協程都被掛起或運行結束,才能去任務隊列找回調\異步任務。
也就是說真·協程對事件循環是有影響的。
或許了解一下nodejs實現協程源碼會更有幫助,但鄙人還未涉獵,這里有篇文章,可以借鑒的查看一下
generator的三個關鍵點
對於一些不怎么接觸generator的朋友,建議記住下面三條概念,即可游刃有余的寫出generator
- next將yield后面的表達式結果作為value
// 下面代碼中的的something會作為next方法返回對象中的value
function* fn () {
yield something;
}
- yield本身沒有返回值,總是返回undefined,但是next可以帶一個參數,作為上一個yield的返回值
// 下面代碼中的a始終的undefined,除非你在下次調用next的時候,傳入參數,那么a就是這個參數
function* fn () {
const a = yield something;
}
- next的參數可以傳遞參數進而改變當前繼續執行的函數上下文環境
最后
目前node v12以上使用的是真正的協程,不是語法糖形式的,可以顯示堆棧暫停、繼續的位置,比如:
async function foo() {
await bar();
return 41;
}
async function bar() {
await Promise.resolve();
throw new Error('ceshiceshi');
}
foo().catch((error) => {
console.log(error.stack);
});
// error info:
//
// Error: ceshiceshi
// at bar (/Users/xx/forTest/app.js:8:11)
// at async foo (/Users/xx/forTest/app.js:2:5)
有興趣的朋友可以試一下。
如有不對,歡迎指正。我會第一時間修改。