JavaScript 單線程模型

2018-07-24 11:51 更新

目錄

含義

單線程模型指的是,JavaScript只在一個線程上運行。也就是說,JavaScript同時只能執(zhí)行一個任務,其他任務都必須在后面排隊等待。

注意,JavaScript只在一個線程上運行,不代表JavaScript引擎只有一個線程。事實上,JavaScript引擎有多個線程,單個腳本只能在一個線程上運行,其他線程都是在后臺配合。

JavaScript之所以采用單線程,而不是多線程,跟歷史有關(guān)系。JavaScript從誕生起就是單線程,原因是不想讓瀏覽器變得太復雜,因為多線程需要共享資源、且有可能修改彼此的運行結(jié)果,對于一種網(wǎng)頁腳本語言來說,這就太復雜了。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節(jié)點上添加內(nèi)容,另一個線程刪除了這個節(jié)點,這時瀏覽器應該以哪個線程為準?所以,為了避免復雜性,從一誕生,JavaScript就是單線程,這已經(jīng)成了這門語言的核心特征,將來也不會改變。

為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創(chuàng)建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準并沒有改變JavaScript單線程的本質(zhì)。

單線程模型帶來了一些問題,主要是新的任務被加在隊列的尾部,只有前面的所有任務運行結(jié)束,才會輪到它執(zhí)行。如果有一個任務特別耗時,后面的任務都會停在那里等待,造成瀏覽器失去響應,又稱“假死”。為了避免“假死”,當某個操作在一定時間后仍無法結(jié)束,瀏覽器就會跳出提示框,詢問用戶是否要強行停止腳本運行。

如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閑著的,因為IO設(shè)備(輸入輸出設(shè)備)很慢(比如Ajax操作從網(wǎng)絡讀取數(shù)據(jù)),不得不等著結(jié)果出來,再往下執(zhí)行。JavaScript語言的設(shè)計者意識到,這時CPU完全可以不管IO設(shè)備,掛起處于等待中的任務,先運行排在后面的任務。等到IO設(shè)備返回了結(jié)果,再回過頭,把掛起的任務繼續(xù)執(zhí)行下去。這種機制就是JavaScript內(nèi)部采用的Event Loop機制。

消息隊列

JavaScript運行時,除了一個運行線程,引擎還提供一個消息隊列(message queue),里面是各種需要當前程序處理的消息。新的消息進入隊列的時候,會自動排在隊列的尾端。

運行線程只要發(fā)現(xiàn)消息隊列不為空,就會取出排在第一位的那個消息,執(zhí)行它對應的回調(diào)函數(shù)。等到執(zhí)行完,再取出排在第二位的消息,不斷循環(huán),直到消息隊列變空為止。

每條消息與一個回調(diào)函數(shù)相聯(lián)系,也就是說,程序只要收到這條消息,就會執(zhí)行對應的函數(shù)。另一方面,進入消息隊列的消息,必須有對應的回調(diào)函數(shù)。否則這個消息就會遺失,不會進入消息隊列。舉例來說,鼠標點擊就會產(chǎn)生一條消息,報告click事件發(fā)生了。如果沒有回調(diào)函數(shù),這個消息就遺失了。如果有回調(diào)函數(shù),這個消息進入消息隊列。等到程序收到這個消息,就會執(zhí)行click事件的回調(diào)函數(shù)。

另一種情況是setTimeout會在指定時間向消息隊列添加一條消息。如果消息隊列之中,此時沒有其他消息,這條消息會立即得到處理;否則,這條消息會不得不等到其他消息處理完,才會得到處理。因此,setTimeout指定的執(zhí)行時間,只是一個最早可能發(fā)生的時間,并不能保證一定會在那個時間發(fā)生。

一旦當前執(zhí)行棧空了,消息隊列就會取出排在第一位的那條消息,傳入程序。程序開始執(zhí)行對應的回調(diào)函數(shù),等到執(zhí)行完,再處理下一條消息。

Event Loop

所謂Event Loop機制,指的是一種內(nèi)部循環(huán),用來一輪又一輪地處理消息隊列之中的消息,即執(zhí)行對應的回調(diào)函數(shù)。

Wikipedia的定義是:“Event Loop是一個程序結(jié)構(gòu),用于等待和發(fā)送消息和事件(a programming construct that waits for and dispatches events or messages in a program)”??梢跃桶袳vent Loop理解成動態(tài)更新的消息隊列本身。

下面是一些常見的JavaScript任務。

  • 執(zhí)行JavaScript代碼
  • 對用戶的輸入(包含鼠標點擊、鍵盤輸入等等)做出反應
  • 處理異步的網(wǎng)絡請求

所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。

同步任務指的是,在JavaScript執(zhí)行進程上排隊執(zhí)行的任務,只有前一個任務執(zhí)行完畢,才能執(zhí)行后一個任務;異步任務指的是,不進入JavaScript執(zhí)行進程、而進入“任務隊列”(task queue)的任務,只有“任務隊列”通知主進程,某個異步任務可以執(zhí)行了,該任務(采用回調(diào)函數(shù)的形式)才會進入JavaScript進程執(zhí)行。

以Ajax操作為例,它可以當作同步任務處理,也可以當作異步任務處理,由開發(fā)者決定。如果是同步任務,主線程就等著Ajax操作返回結(jié)果,再往下執(zhí)行;如果是異步任務,該任務直接進入“任務隊列”,JavaScript進程跳過Ajax操作,直接往下執(zhí)行,等到Ajax操作有了結(jié)果,JavaScript進程再執(zhí)行對應的回調(diào)函數(shù)。

也就是說,雖然JavaScript只有一個進程用來執(zhí)行,但是并行的還有其他進程(比如,處理定時器的進程、處理用戶輸入的進程、處理網(wǎng)絡通信的進程等等)。這些進程通過向任務隊列添加任務,實現(xiàn)與JavaScript進程通信。

想要理解Event Loop,就要從程序的運行模式講起。運行以后的程序叫做“進程”(process),一般情況下,一個進程一次只能執(zhí)行一個任務。如果有很多任務需要執(zhí)行,不外乎三種解決方法。

  1. 排隊。因為一個進程一次只能執(zhí)行一個任務,只好等前面的任務執(zhí)行完了,再執(zhí)行后面的任務。

  2. 新建進程。使用fork命令,為每個任務新建一個進程。

  3. 新建線程。因為進程太耗費資源,所以如今的程序往往允許一個進程包含多個線程,由線程去完成任務。

如果某個任務很耗時,比如涉及很多I/O(輸入/輸出)操作,那么線程的運行大概是下面的樣子。

synchronous mode

上圖的綠色部分是程序的運行時間,紅色部分是等待時間??梢钥吹剑捎贗/O操作很慢,所以這個線程的大部分運行時間都在空等I/O操作的返回結(jié)果。這種運行方式稱為”同步模式”(synchronous I/O)。

如果采用多線程,同時運行多個任務,那很可能就是下面這樣。

synchronous mode

上圖表明,多線程不僅占用多倍的系統(tǒng)資源,也閑置多倍的資源,這顯然不合理。

asynchronous mode

上圖主線程的綠色部分,還是表示運行時間,而橙色部分表示空閑時間。每當遇到I/O的時候,主線程就讓Event Loop線程去通知相應的I/O程序,然后接著往后運行,所以不存在紅色的等待時間。等到I/O程序完成操作,Event Loop線程再把結(jié)果返回主線程。主線程就調(diào)用事先設(shè)定的回調(diào)函數(shù),完成整個任務。

可以看到,由于多出了橙色的空閑時間,所以主線程得以運行更多的任務,這就提高了效率。這種運行方式稱為”異步模式“(asynchronous I/O)。

這正是JavaScript語言的運行方式。單線程模型雖然對JavaScript構(gòu)成了很大的限制,但也因此使它具備了其他語言不具備的優(yōu)勢。如果部署得好,JavaScript程序是不會出現(xiàn)堵塞的,這就是為什么node.js平臺可以用很少的資源,應付大流量訪問的原因。

如果有大量的異步任務(實際情況就是這樣),它們會在“消息隊列”中產(chǎn)生大量的消息。這些消息排成隊,等候進入主線程。本質(zhì)上,“消息隊列”就是一個“先進先出”的數(shù)據(jù)結(jié)構(gòu)。比如,點擊鼠標就產(chǎn)生一系列消息(各種事件),mousedown事件排在mouseup事件前面,mouseup事件又排在click事件的前面。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號