JavaScript 單線(xiàn)程模型

2018-07-24 11:51 更新

目錄

含義

單線(xiàn)程模型指的是,JavaScript只在一個(gè)線(xiàn)程上運(yùn)行。也就是說(shuō),JavaScript同時(shí)只能執(zhí)行一個(gè)任務(wù),其他任務(wù)都必須在后面排隊(duì)等待。

注意,JavaScript只在一個(gè)線(xiàn)程上運(yùn)行,不代表JavaScript引擎只有一個(gè)線(xiàn)程。事實(shí)上,JavaScript引擎有多個(gè)線(xiàn)程,單個(gè)腳本只能在一個(gè)線(xiàn)程上運(yùn)行,其他線(xiàn)程都是在后臺(tái)配合。

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

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

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

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

消息隊(duì)列

JavaScript運(yùn)行時(shí),除了一個(gè)運(yùn)行線(xiàn)程,引擎還提供一個(gè)消息隊(duì)列(message queue),里面是各種需要當(dāng)前程序處理的消息。新的消息進(jìn)入隊(duì)列的時(shí)候,會(huì)自動(dòng)排在隊(duì)列的尾端。

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

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

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

一旦當(dāng)前執(zhí)行??樟?,消息隊(duì)列就會(huì)取出排在第一位的那條消息,傳入程序。程序開(kāi)始執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù),等到執(zhí)行完,再處理下一條消息。

Event Loop

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

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

下面是一些常見(jiàn)的JavaScript任務(wù)。

  • 執(zhí)行JavaScript代碼
  • 對(duì)用戶(hù)的輸入(包含鼠標(biāo)點(diǎn)擊、鍵盤(pán)輸入等等)做出反應(yīng)
  • 處理異步的網(wǎng)絡(luò)請(qǐng)求

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

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

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

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

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

  1. 排隊(duì)。因?yàn)橐粋€(gè)進(jìn)程一次只能執(zhí)行一個(gè)任務(wù),只好等前面的任務(wù)執(zhí)行完了,再執(zhí)行后面的任務(wù)。

  2. 新建進(jìn)程。使用fork命令,為每個(gè)任務(wù)新建一個(gè)進(jìn)程。

  3. 新建線(xiàn)程。因?yàn)檫M(jìn)程太耗費(fèi)資源,所以如今的程序往往允許一個(gè)進(jìn)程包含多個(gè)線(xiàn)程,由線(xiàn)程去完成任務(wù)。

如果某個(gè)任務(wù)很耗時(shí),比如涉及很多I/O(輸入/輸出)操作,那么線(xiàn)程的運(yùn)行大概是下面的樣子。

synchronous mode

上圖的綠色部分是程序的運(yùn)行時(shí)間,紅色部分是等待時(shí)間。可以看到,由于I/O操作很慢,所以這個(gè)線(xiàn)程的大部分運(yùn)行時(shí)間都在空等I/O操作的返回結(jié)果。這種運(yùn)行方式稱(chēng)為”同步模式”(synchronous I/O)。

如果采用多線(xiàn)程,同時(shí)運(yùn)行多個(gè)任務(wù),那很可能就是下面這樣。

synchronous mode

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

asynchronous mode

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

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

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

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

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)