詭異的JS非同步單執行緒是如何工作的
對於通常的developer(特別是那些具備平行計算/多執行緒背景知識的developer)來講,js的非同步處理著實稱得上詭異。而這個詭異從結果上講,是由js的“單執行緒”這個特性所導致的。
我曾嘗試用“先定義後展開”的教科書方式去講解這一塊的內容,但發現極其痛苦。因為要理清楚這個東西背後的細節,並將其泛化、以更高的視角來看問題,著實涉及非常多的基礎知識。等到我把這些知識講清楚、講完,無異於逼迫讀者抱著作業系統、計算機網路這樣的催眠書看上好個幾章節,著實沉悶而乏味。
並且更關鍵的是,在走到那一步的時候,讀者的精力早已消耗殆盡,完全沒有心力再去關心這個最開始的問題——js的非同步處理為何詭異。
所以,我決定反過來,讓我們像一個初學者那樣,從一無所知開始,
先使用“錯誤的理念”去開始我們的討論,然後用程式碼去發現和理念相違背的地方。
再做出一些修正,再考察一些例子,想想是否還有不大滿意和清楚的地方,再調整。如此往復,我們會像偵探那樣,先從一個不大正確的假設開始,不斷尋找證據,不斷修正假設,一步步追尋下去,直到抵達最後完整的真相。
我想,這樣的寫作方式,更符合一個人真正的求知和研究過程,並能夠為你帶來更多關於“探索問題”的啟發。我想,這樣的思維方式和研究理念,比普通的知識更為重要。它能夠讓你成為知識的獵人,有能力獨立地覓食,而不必被迫成為嬰孩,只能坐等他人餵食。
好了,讓我們先從一塊js程式碼,開始我們的探索之旅。
console.log(`No. 1`);
setTimeout(function(){
console.log(`setTimeout callback`);
}, 5000);
console.log(`No. 2`);
輸出結果是:
No. 1
No. 2
setTimeout callback
這塊程式碼中幾乎沒什麼複雜的東西,全是列印語句。唯一的特別是函式setTimeout
,根據粗略的網上資料顯示,它接受兩個引數:
- 第一個引數是callback函式,就是讓它執行完之後,回過頭來呼叫的函式。
- 另一個是時間引數,用於指定多少微妙之後,執行callback函式。這裡我們使用了5000微妙,也即是5秒鐘。
另一個重點是,setTimeout
是一個非同步函式,意思是我的主程式不必去等待setTimeout
執行完畢,將它的執行過程扔到別的地方執行,然後主程式繼續往下走。也即是,主程式是一個步調、setTimeout
是另一個步調,即是“非同步”的方式跑程式碼。
如果你有一些平行計算或者多執行緒程式設計的背景知識,那麼上面的語句就再熟悉不過了。如果在多執行緒環境,無非是另起一根執行緒去執行列印語句console.log(`setTimeout callback`)
。然後主執行緒繼續往下走,新執行緒去負責列印語句,清晰明瞭。
所以綜合起來,這段程式碼的意思是,主執行緒執行到語句setTimeout
時,就把它交給“其它地方”,讓這個“其它地方”等待5秒鐘之後執行。而主執行緒繼續往下走,去執行“No. 2”的列印。所以,由於其它部分要等待5秒鐘之後才執行,而主執行緒立刻往下執行了“No. 2”的列印,最終的輸出結果才會是先列印“No. 2”,再列印“setTimeout callback”。
嗯,so far so good。一切看來都比較美好。
如果我們對上述程式做一點變動呢?例如,我可不可以讓“setTimeout callback”這個資訊先被列印出來呢?因為在平行計算中,我們經常遇到的問題便是,由於你不知道多個執行緒之間誰執行得快、誰執行得慢,所以我們無法判定最終的語句執行順序。這裡我們讓“setTimeout callback”停留了5秒鐘,時間太長了,要不短一點?
console.log(`No. 1`);
setTimeout(function(){
console.log(`setTimeout callback`);
}, 1);
console.log(`No. 2`);
我們將傳遞給setTimeout
的引數改成了1毫秒。多次執行後會發現,結果竟然沒有改變?!似乎有點反常,要不再改小一點?改成0?
console.log(`No. 1`);
setTimeout(function(){
console.log(`setTimeout callback`);
}, 0);
console.log(`No. 2`);
多次執行後,發現依舊無法改變。這其實是有點奇怪了。因為通常的平行計算、多執行緒程式設計中,通過多次執行,你其實是可以看到各種無法預期的結果的。在這裡,竟然神奇地得到了相同的執行順序結果。這就反常了。
但我們還無法完全下一個肯定的結論,可不可能因為是setTimeout
的啟動時間太長,而導致“No. 2”這條語句先被執行呢?為了做進一步的驗證,我們可以在“No. 2”這條列印語句之前,加上一個for
迴圈,給setTimeout
充分的時間去啟動。
console.log(`No. 1`);
setTimeout(function(){
console.log(`setTimeout callback`);
}, 0);
for (let i = 0; i < 10e8; i++) {}
console.log(`No. 2`);
執行這段程式碼,我們發現,”No. 1″這條列印語句很快地顯示到了瀏覽器命令列,等了一秒鐘左右,接著輸出了
No. 2
setTimeout callback
誒?!這不就更加奇怪了嗎?!setTimeout
不是等待0秒鐘後立刻執行嗎,就算啟動再慢,也不至於等待一秒鐘之後,還是無法正常顯示吧?況且,在加入這個for
迴圈之前,“setTimeout callback”這條輸出不是立刻就顯示了嗎?
綜合這些現象,我們有理由懷疑,似乎“setTimeout callback”一定是在“No. 2”後顯示的,也即是:setTimeout
的callback函式,一定是在console.log(`No. 2`)
之後執行的。為了驗證它,我們可以做一個危險一點的測試,將這個for
迴圈,更改為無限while
迴圈。
console.log(`No. 1`);
setTimeout(function(){
console.log(`setTimeout callback`);
}, 0);
while {} // dangerouse testing
console.log(`No. 2`);
如果setTimeout
的callback函式是按照自己的步調做的執行,那麼它就有可能在某個時刻列印出“setTimeout callback”。而如果真的是按照我們猜測的那樣,“setTimeout callback”必須排在“No. 2”之後,那麼瀏覽器命令列就永遠不會出現“setTimeout callback”。
執行後發現,在瀏覽器近乎要臨近崩潰、達到記憶體溢位的情形下,“setTimeout callback”依舊沒有列印出來。這也就證明了我們的猜測!
這裡,我們第一次出現了理念和現實的矛盾。按照通常平行計算的理念,被扔到“其它地方”的setTimeout
callback函式,應該被同時執行。可事實卻是,這個“其它地方”並沒有和後一條列印“No. 2”的語句共同執行。這時候,我們就必須要回到基礎,回到js這門語言底層的實現方式上去追查,以此來挖掘清楚這後面的貓膩。
js的特性之一是“單執行緒”,也即是從頭到尾,js都在同一根執行緒下執行。或許這是一個值得調查深入的點。想來,如果是多執行緒,那麼setTimeout
也就該按照我們原有的理念做執行了,但事實卻不是。而這兩者的不同,便在於單執行緒和多執行緒上。
找到了這個不同點,我們就可以更深入去思考一些細節。細想起來,所謂“非同步”,就是要開闢某個“別的地方”,讓“別的地方”和你的主執行路線一起執行。可是,如果現在是單執行緒,也就意味著計算資源有且只有一份,請問,你如何做到“同時執行”呢?
這就好比是,如果你去某個辦事大廳,去繳納水費、電費、天然氣。那麼,我們可以粗略地將它們分為水費櫃檯、電費櫃檯、天然氣櫃檯。那麼,如果我們依次地“先在水費櫃檯辦理業務,等到水費的明細列印完畢、繳納完費用後;再跑去電費櫃檯列印明細、繳納費用;再跑去天然氣櫃檯列印明細、繳納費用”,這就是一個同步過程,必須等待上一個步驟做完後,才能做下一步。
而非同步呢,就是說我們不必在某個環節浪費時間瞎等待。比如,我們可以在“列印水費明細”的空閒時間,跑到電費和天然氣櫃檯去辦理業務,將“電費明細、天然氣明細的列印”這兩個任務提前啟動起來。再回過頭去繳納水費、繳納電費、繳納天然氣費用。其實,這就是華羅庚推廣優選法的時候舉的例子,燒水、倒茶葉、泡茶,如何安排他們的順序為高效。
顯然,非同步地去做任務更高效。但這要有一個前提,就是你做任務的資源,也即是幹活的人或者機器,得有多份才行。同樣按照上面的例子來展開討論,雖然有水費、電費、天然氣這三個櫃檯,可如果這三個櫃檯背後的辦事人員其實只有一個呢?比如你啟動了辦理水費的業務,然後想要在辦理水費業務的等待期,去電費櫃檯辦理電費業務。表面上,你去電費櫃檯下了申請單,請求辦理電費業務,可卻發現根本沒有辦事員去接收你的這個業務!為何?因為這有且只有一個的辦事員,還正在辦理你的水費業務啊!這時候,你的這個所謂的“非同步”,有何意義?!
所以從這個角度來看,當計算資源只有一份的時候,你做“非同步”其實是沒什麼意義的。因為幹活的資源只有一份,就算在表面做了名義上的“非同步”,可最終就像上面的多櫃檯單一辦事員那樣,到了執行任務層面,還是會一個接一個地完成任務,這就沒有意義了。
那麼,js的特性是”單執行緒“+”非同步“,不就正是我們討論的“沒有意義”的情況嗎?!那又為何要多次一舉,幹一些沒有意義的事情呢?
嗯……事情變得越來越有趣了。
通常來講,如果一個事件出現了神奇和怪異的地方,基本上都是因為我們忽略了某個細節,或者對某個細節存在誤解或是錯誤理解。要想把問題解決,我們就必須不斷地回顧已有材料,在不斷地重複檢驗中,發現那幾根我們忽略的貓膩。
讓我們回顧一下關於js非同步的宣傳片。通常為了說明js非同步的必要性,會舉出瀏覽器的資源載入和頁面渲染這個矛盾。
渲染,可以比較粗糙地理解為將“畫面”畫出來的過程。例如,瀏覽器要將頁面上的按鈕、圖片顯示出來,就必須有一個將“圖片”在網頁上畫出來的動作。又或是,作業系統要將“桌面”這個圖形介面顯示在顯示器上,就必須要把它相應的這個“畫面”在顯示器上畫出來的動作。歸結起來,這個“畫出來”的過程,就被稱之為“渲染”。
例如,你點選頁面上的一個button,讓瀏覽器去後端資料庫將資料包表取出來,在網頁上把數字顯示出來。而如果js不支援非同步的話,整個網頁的就會停留,也即是“卡”,在滑鼠點選按鈕這一個動作上,頁面無法完成後續的渲染工作。一直要等到後端把資料返回到了前端,程式流才能夠繼續跑下去。
所以這裡,js的“非同步”其實是為了讓瀏覽器將“載入”這個任務分給“其它地方”,讓“載入過程”和“渲染過程”同步進行下去。
等等,又是這個“其它地方”?!!
我擦,不是說js是單執行緒而麼,計算資源不是隻有一份麼,怎麼又可以“一邊載入、一邊渲染”了?!WTF,你這是在逗我玩兒麼?!
艹,到底這裡面哪句話是真的?!到底js是單執行緒是真的?還是說瀏覽器可以同時做“一邊載入、一邊渲染”這個事情是真的?!
如何才能解決這個疑惑?!很顯然,我們必須要深入到瀏覽器的內部,去看一看它到底是怎麼樣被設計的。
在搜尋引擎中,做一些關於瀏覽器和js的搜尋,我們不難得到一些基本資訊。js並不是瀏覽器的全部,瀏覽器要掌管的事情太多了,掌管js的只是瀏覽器的一個元件,叫做js引擎。而最出名的、並在Chrome中使用的,就是大名鼎鼎的V8引擎,它負責js的解析和執行。
另一方面我們還知道,使用js的一個很大原因,是因為它能夠自由地去操控DOM元素、去執行Ajax非同步請求、能夠像我們最開始舉的例子那樣,使用setTimeout
做非同步任務分配。這些都是js優秀特性。
可令人驚訝的事情來了,當我們去探索這個掌管js一切的V8引擎的時候,我們卻發現,它並不提供DOM的操控、Ajax的執行以及setTimeout
的特性:
上圖來自Alexander Zlatkov,它的結構是:
-
JS引擎
- Memory Heap
- Call Stack
-
Web APIs
- DOM (Document)
- Ajax (XMLHttpRequest)
- Timeout (setTimeout)
- Callback Queue
- Event Loop
明明是js的特性,為什麼這些職能卻不是由js的引擎來掌管呢?嗯,interesting~~~
誒!不是“單執行緒”麼,不是載入過程被扔到其它地方麼?!js是單執行緒,也即是js在js引擎中是單執行緒、只能夠分到一份計算資源,可是,載入資料的Ajax這個feature不是沒有被放到js引擎麼?!
艹!真TM是老狐狸啊!還以為“單執行緒”和“一邊載入、一邊渲染”這兩種說法只有一種是對的,可結果是,都對!為什麼呢?因為只說了js是單執行緒,可沒說瀏覽器本身是單執行緒啊!所以咯,渲染相關的js部分可以和資料載入的Ajax部分是可以同時進行的,因為它們根本就在兩個模組,即兩個執行緒嘛!所以當然可以並行啊!WTF!
誒~等等,讓我們再仔細看看上面這張圖呢?!Ajax不在js引擎裡,可是setTimeout
也不在js引擎裡面啊!!如果Web APIs這部分是在不同於js引擎的另外一根執行緒裡,它們不就可以實現真正意義上的並行嗎?!那為何我們開頭的列印資訊“setTimeout callback”,無法按照並行的方式,優先於“No. 2”列印出來呢?
嗯……真是interesting……事情果然沒有那麼簡單。
顯然,我們需要考察更多的細節,特別是,每一條語句在上圖中,是按照什麼順序被移動、被執行的。
談到語句的執行順序,我們需要再一次將關注點放回到js引擎上。再次回看上面這幅結構圖,JS引擎包含了兩部分:一個是 memory heap,另一個是call stack。前者關於記憶體分配,我們可以暫時放下。後面即是函式棧,嗯,它就是要進一步理解執行順序的東西。
函式棧(call stack)為什麼要叫做“棧(stack)”呢?為什麼不是叫做函式佇列或者別的神馬?這其實可以從函式的執行順序上做一個推斷。
函式最開始被引進,其實就是為了程式碼複用和模組化。我們期望一段本該出現的程式碼,被單獨提出來,然後只需要用一個函式呼叫,就可以將這段程式碼的執行內容給插入進來。
所以,如果當我們執行一段程式碼時,如果遇到了函式呼叫,我們會期望先去將函式裡面的內容執行了,再跳出來回到主程式流,繼續往下執行。
所以,如果把一個函式看作函式節點的話,整個執行流程其實是關於函式節點的“深度優先”遍歷,也即是從主函式開始執行的函式呼叫,整個呈深度優先遍歷的方式做呼叫。而結合演算法和資料結構的知識,我們知道,要實現“深度遍歷”,要麼使用遞迴、要麼使用stack這種資料結構。而後者,無疑更為經濟使用。
所以咯,既然期望函式呼叫呈深度優先遍歷,而深度優先遍歷又需要stack這種資料結構做支援,所以維護這個函式呼叫的結構就當然呈現為stack的形式。所以叫做函式棧(stack)。
當然,如果再發散思考一下,作業系統的底層湧來維護函式呼叫的部分也叫做函式棧。那為何不用遞迴的方式來實現維護呢?其實很簡單,計算機這麼個啥都不懂的東西,如何知道遞迴和返回?它只不過會一往無前的一直執行命令而已。所以,在沒有任何輔助結構的情況下,能夠一往無前地執行的方式,只能是stack,而不是更為複雜的遞迴概念的實現。
另一方面,回到我們最開頭的問題,矛盾其實是出現在setTimeout
的callback函式上。而上面的結構圖裡,還有一部分叫做“callback queue”。顯然,這一部分也是我們需要了解的東西。
結合js的call stack和callback queue這兩個關鍵詞,我們不難搜尋到一些資料,來展開討論這兩部分是如何同具體的語句執行相結合的。
先在整體上論述一下這個過程:
- 正常的語句執行,會一條接一條地壓入call stack,執行,再根據執行的內容繼續壓入stack。
- 而如果遇到有Web APIs相關的語句,則會將相應的執行內容扔到Web APIs那邊。
- Web APIs這邊,可以獨立於js引擎,並行地分配給它的語句,如Ajax資料載入、
setTimeout
的內容。 - Web APIs這邊的callback function,會在在執行完相關語句後,被扔進“callback queue”。
- Event loop會不斷地監測“call stack”和“callback queue”。當“call stack”為空的時候,event loop會將“callback queue”裡的語句壓入到stack中,繼續做執行。
- 如此迴圈往復。
以上內容比較抽象,讓我們用一個具體的例子來說明。這個例子同樣來自於Alexander Zlatkov。使用它的原因很簡單,因為Zlatkov在blog中使用的說明圖,實在是相當清晰明瞭。而目前我沒有多餘的時間去使用PS繪製相應的結構圖,就直接拿來當作例子說明了。
讓我們考察下面的程式碼片段:
console.log(`Hi`);
setTimeout(function cb1() {
console.log(`cb1`);
}, 5000);
console.log(`Bye`);
哈哈,其實和我們使用的程式碼差不多,只是列印的內容不同。此時,在執行之前,整個底層的結構是這樣的:
然後,讓我們執行第一條語句console.log(`Hi`)
,也即是將它壓入到call stack中:
然後js引擎執行stack中最上層的這條語句。相應的,瀏覽器的控制檯就會列印出資訊“Hi”:
由於這條語句被執行了,所以它也從stack中消失:
再來壓入第二條語句setTimeout
:
執行setTimeout(function cb1() { console.log(`cb1`); }, 5000);
:
注意,由於setTimout
部分並沒有被包含在js引擎中,所以它就直接被扔給了Web APIs的Timeout
部分。這裡,stack中的藍色部分起到的作用,就是將相應的內容“timer、等候時間5秒、回撥函式cb1”扔給Web APIs。然後這條語句就可以從stack中消失了:
繼續壓入下一條語句console.log(`Bye`)
:
注意,此時在Web APIs的部分,正在並行於js引擎執行相應的語句,即:等候5秒鐘。Okay,timer繼續它的等待,而stack這邊已經有語句了,所以需要把它執行掉:
相應的瀏覽器控制檯,就會顯示出“Bye”的資訊。而stack中執行後的語句,就該消失:
此時,stack已經為空。Event loop檢測到stack為空,自然就想要將callback queue中的語句壓入到stack中。可此時,callback queue中也為空,於是Event loop只好繼續迴圈檢測。
另一方面,Web APIs這邊的timer,並行地在5秒鐘後開始了它的執行——什麼也不做。然後,將它相應的回撥函式cb1()
,放到callback queue中:
Event loop由於一直在迴圈檢測,此時,看到callback queue有了東西,就迅速將它從callback queue中取出,然後將其壓入到stack裡:
現在Stack裡有了東西,就需要執行回撥函式cb1()
。而cb1()
裡面呼叫了 console.log(`cb1`)
這條語句,所以需要將它壓入stack中:
stack繼續執行,現在它的最上層是 console.log(`cb1`)
,所以需要先執行它。於是瀏覽器的控制它列印出相應的資訊“cb1”:
將執行了的 console.log(`cb1`)
語句彈出棧:
繼續執行cb1()
剩下的語句。此時,cb1()
已經沒有其它需要執行的語句了,也即是它被執行完畢,所以,將它也從stack中彈出:
整個過程結束!如果從頭到尾看一遍的話,就是下面這個gif圖了:
相當清晰直觀,對吧!
如果你想進一步地把玩js的語句和call stack、callback queue的關係,推薦Philip Roberts的一個GitHub的開源專案:Loupe,裡面有他online版本供你做多種嘗試。
有了這些知識,現在我們回過頭去看開頭的那段讓人產生疑惑的程式碼:
console.log(`No. 1`);
setTimeout(function(){
console.log(`setTimeout callback`);
}, 0);
console.log(`No. 2`);
按照上面的js處理語句的順序,第一條語句console.log(`No. 1`)
會被壓入stack中,然後被執行的是setTimout
。
根據我們上面的知識,它會被立刻扔進Web APIs中。可是,由於這個時候我們給它的等待時間是0,所以,它的callback函式console.log(`setTimeout callback`)
會立刻被扔進“Callback Queue”裡面。所以,那個傳說中的“其它地方”指的就是callback queue。
那麼,我們能夠期望這一條console.log(`setTimeout callback`)
先於“No. 2”被列印出來嗎?
其實是不可能的!為什麼?因為要讓它被執行,首先它需要被壓入到call stack中。可是,此時call stack還沒有將程式的主分支上的語句執行完畢,即還有console.log(`No. 2`)
這條語句。所以,event loop在stack還未為空的情況下,是不可能把callback queue的語句壓入stack的。所以,最後一條“setTimeout callback”的資訊,一定是會排在“No. 2”這條資訊後面被列印出來的!
這完全符合我們之前加入無限while
迴圈的結果。因為主分支一直被while
迴圈佔有,所以stack就一直不為空,進而,callback queue裡的列印“setTimeout callback”的語句就更不可能被壓入stack中被執行。
探索到這裡,似乎該解決的問題也都解決了,好像就可以萬事大吉,直接封筆走人了。可事實卻是,這才是我們真正的泛化討論的開始!
做研究和探索,如果停留於此,就無異於小時候自己交作業給老師,目的僅僅是完成老師佈置的任務。在這裡,這個老師佈置的任務就是文章開頭所提出的讓人疑惑的程式碼。可是,解決這段程式碼並不是我們的終極目的。我們需要泛化我們的所學和所知,從更深層次的角度去探索,為什麼我們會疑惑,為什麼一開始無法發現這些潛藏在表面之下不同。我們要繼續去挖掘,我們到底在哪些最根本的問題上出現了誤解和錯誤認識,從而導致我們一路如此辛苦,無法在開頭看到事情的真相。
回顧我們的歷程,一開始讓我們載跟斗的,其實就是對“非同步”和“多執行緒”的固定假設。多執行緒了,就是非同步,而非同步了,一定是多執行緒嗎?我們潛意識裡是很想做肯定回答的。這是因為如果非同步了,但卻是單執行緒,整個非同步就沒有意義了(回憶那個多櫃檯、單一辦事員的例子)。可js卻巧妙地運用了:使用非同步單執行緒去分配任務,而讓真正做資料載入的Ajax、或者時間等待的setTimeout的工作,扔給瀏覽器的其它執行緒去做。所以,本質上js雖然是單執行緒的,可在做實際工作的時候,卻利用了瀏覽器自身的多執行緒。這就好比是,雖然是多櫃檯、單一辦事員,可辦事員將繳納電費、水費的任務,外包給其它公司去做,這樣,雖然自己仍然是一個辦事員,但卻由於有了外包服務的支援,依舊可以一起並行來做。
另一方面,js的非同步、單執行緒的特性,逼迫我們去把平行計算中的“同步/非同步、阻塞/非阻塞”等概念理得更清楚。
“同步”的英文是synchronize,但在中文的語境下,卻很容易和“同時”掛鉤。於是,在潛意識裡有可能會有這樣一種聯想,“同步”就是“同時”,所以,一個同步(synchronize)的任務就被理解為“可以一邊做A,一邊做B”。而這個潛意識的印象,其實完全是錯誤的(一般做A一邊做B,其實是“非同步”+“並行”的情況)。
但在各類百科詞典上,確實有用“同時”來作為對“同步”的解釋。這是為什麼呢?其實這是對”同步“用作”同時“的一個混淆理解。如果仔細考慮”同時“的意思,細分起來,其實是有兩種理解:
- 同一個時刻(at the same time),例如在9:00 a.m這個時間點,我們既在做A也在做B。
- 另一個是同一個時間參考系,也就是所謂的clock on the wall是同一個。
前者很容易理解,這裡我重點解釋一下後者。例如,我在中國大陸同美國的一個同學開微信語音聊天,我這邊是22:00,他那邊是9:00。我們做聊天這件事情的時候,是同一時刻(at the same time),但卻不在同一個時間參考體系(clock on the wall)。而在計算機中討論的同步,其實討論的是後者的”同一參考系“,同步,就是讓我們的參考系統一起來,放到同一個體系之下。
又比如,我們在生活中很容易說,同步你的電腦、同步你的手機通訊錄、同步你的相簿,說的是什麼呢?就是讓你的各個客戶端:PC、手機,同server端伺服器的內容都保持一致,也即是大家都被放到一個一致的參考系裡面。不要說,你在PC裡有照片A,而在手機裡沒有A卻有B,這個時候,談論PC裡資訊人與談論手機裡資訊的人,就是在雞同鴨講。究其原因,就是沒有把大家放到同一個參考系裡面。
所以,同步synchronize所指的”同時“,是大家把牆上的時鐘都調整到一致、調整為同一個步調,也即是同時、同一時間參考系的意思。而不是說,讓事情在同一時刻並列發生。自然的,什麼是非同步(asynchronize)呢,非同步就是大家的時間參考系是不同的,例如我在中國大陸、你在美國,我們的時間參考體系是不同的,這就是非同步,不在同一個步調、頻段上。
事實上,每一個獨立的人、每一塊獨立的計算資源,它都代表了一個各自的參考體系。只要你將任務分發給了其他人或是其它計算資源,此時,就出現了兩個參考體系:一個是原有主分支的參考體系,另一個是新的計算資源的參考體系。在平行計算中,有一個同步機制是使用語句barrier,目的是讓所有的計算分支在這一個位置節點都完成了計算。為什麼說它是一種同步機制?按照我們統一參考體系的理解,就是保證其他所有計算分支完成計算,也就保證了其它分支的消失,從而只剩下主分支這一個參考體系。於是大家可以談論同樣的東西,說同樣的話,不會有誤解。
另一方面,如果要更深入地理解js的設計,我認為還需要回到計算機歷史的初期,例如那個只有單核的分時系統的時代。在那樣一個時代,作業系統所受到的硬體上的限制,不亞於js引擎在瀏覽器中所受到的限制。在同樣的限制下,曾經的作業系統會如何去巧妙運用那極為有限的計算資源,讓整個作業系統給人以平滑、順暢和功能強大的錯覺?我想,js的這些設計必定和作業系統早期的設計緊密相關。所以在這個層面上,它將再一次回到作業系統這樣的基礎知識上。能否吃透現代的技術,其實很大層面上取決於你是否吃透了設計的歷史,是否理解在那些資源枯竭的年代,各路大神是如何巧妙地逢山開路,遇水搭橋。無論現代的計算機硬體資源有多麼豐富,它必定會因為目標的主次關係、業務的主次關係受到限制。而如何在限制中跳舞和創造,這是能夠貫穿整個歷史的共同性問題。
Reference:
- How JavaScript works: Event loop and the rise of Async programming + 5 ways to better coding with async/await
- asynchronous vs non-blocking
- 非同步(Asynchronous)與同步(Synchronous)的差異
- Philip Roberts: What the heck is the event loop anyway? | JSConf EU
近期回顧
《沒有idea這把米,怎麼炊熟創業這碗飯》
《2018年08月寫字總結》
《財務自由所虛構的妄念》
如果你喜歡我的文章或分享,請長按下面的二維碼關注我的微信公眾號,謝謝!
更多資訊交流和觀點分享,可加入知識星球:
相關文章
- 單執行緒的js是如何工作的執行緒JS
- 非同步/同步,阻塞/非阻塞,單執行緒/多執行緒概念梳理非同步執行緒
- 一個執行緒罷工的詭異事件執行緒事件
- 如何理解JS的單執行緒?JS執行緒
- JS單執行緒和非同步JS執行緒非同步
- 淺談JS中的非同步和單執行緒JS非同步執行緒
- 多執行緒Demo學習(執行緒的同步,簡單的執行緒通訊)執行緒
- 執行緒的同步執行緒
- node.js的非同步I/O、事件驅動、單執行緒Node.js非同步事件執行緒
- JS定時器和單執行緒非同步特性JS定時器執行緒非同步
- 對執行緒、協程和同步非同步、阻塞非阻塞的理解執行緒非同步
- 執行緒池是怎樣工作的?執行緒
- 執行緒池中多餘的執行緒是如何回收的?執行緒
- 程式執行緒、同步非同步、阻塞非阻塞、併發並行執行緒非同步並行
- suging閒談-netty 的非同步非阻塞IO執行緒與業務執行緒分離Netty非同步執行緒
- Java多執行緒—執行緒同步(單訊號量互斥)Java執行緒
- 瀏覽器多執行緒和js單執行緒瀏覽器執行緒JS
- 單執行緒-非阻塞-長連結執行緒
- 什麼是程式(執行緒)同步執行緒
- 併發-0-同步/非同步/阻塞/非阻塞/程式/執行緒非同步執行緒
- 聊聊執行緒與程式 & 阻塞與非阻塞 & 同步與非同步執行緒非同步
- Java中的執行緒同步Java執行緒
- 單執行緒的 Javascript 為什麼可以非同步執行緒JavaScript非同步
- 單執行緒的JS如何實現多個互動同時進行執行緒JS
- 簡單分析ThreadPoolExecutor回收工作執行緒的原理thread執行緒
- Node.js 的單執行緒事件驅動模型和內建的執行緒池模型Node.js執行緒事件模型
- JavaScript 如何在後臺工作:瞭解其單執行緒性質和非同步操作JavaScript執行緒非同步
- 多執行緒和多執行緒同步執行緒
- 寶付初識單執行緒的 Node.js執行緒Node.js
- redis自學(22)Redis是單執行緒還是多執行緒?Redis執行緒
- 好程式設計師web前端分享如何理解JS的單執行緒程式設計師Web前端JS執行緒
- 譯—JavaScript是如何工作的(1):js引擎、執行時和呼叫棧的概述JavaScriptJS
- 伺服器模型——從單執行緒阻塞到多執行緒非阻塞(下)伺服器模型執行緒
- 伺服器模型——從單執行緒阻塞到多執行緒非阻塞(中)伺服器模型執行緒
- 程式與執行緒、同步與非同步、阻塞與非阻塞、併發與並行執行緒非同步並行
- 最全java多執行緒總結2--如何進行執行緒同步Java執行緒
- 多執行緒程式是如何執行程式碼的?執行緒行程
- 多執行緒的同步和非同步學習執行緒非同步