瀏覽器是如何調(diào)度進(jìn)程和線程的?
最近正值秋招,面試了很多前端同學(xué),感悟頗多,后面我也會在公眾號為大家分享下我作為面試官的一些心得,以及對于我經(jīng)常會問的一些問題的講解。
今天我們來聊一下瀏覽器(以Chrome為例)對線程和進(jìn)程的調(diào)度,這個問題幾乎是我每次面試必問的。相信大家都看過很多面經(jīng)會講 JavaScript 的執(zhí)行機(jī)制,很多同學(xué)熱衷于去背這些面經(jīng),以至于連 JavaScript 是單線程的都不知道,就開始回答宏任務(wù)、微任務(wù)了... 這種我真的特別無語,是真的理解還是背出來的解題思路其實一看便知了。所以我建議大家無論是準(zhǔn)備面試還是平時積累知識,一定不要太浮躁,要從根本上理解這個問題,而不是去記這些解題思路。
線程和進(jìn)程
首先我們來回顧下線程和進(jìn)程的概念:
- 進(jìn)程:CPU 進(jìn)行資源分配的基本單位
- 線程:CPU 調(diào)度的最小單位
這是進(jìn)程和線程最官方也是最常見的兩個定義,但是這兩個概念太抽象了,很難以理解。通俗一點講:進(jìn)程可以描述為一個應(yīng)用程序的執(zhí)行程序,線程則是進(jìn)程內(nèi)部用來執(zhí)行某個部分的程序。
下面再引用一段知乎的高贊回答,我感覺非常有意思:
做個簡單的比喻:進(jìn)程=火車,線程=車廂
- 線程在進(jìn)程下行進(jìn)(單純的車廂無法運行)
- 一個進(jìn)程可以包含多個線程(一輛火車可以有多個車廂)
- 不同進(jìn)程間數(shù)據(jù)很難共享(一輛火車上的乘客很難換到另外一輛火車,比如站點換乘)
- 同一進(jìn)程下不同線程間數(shù)據(jù)很易共享(A車廂換到B車廂很容易)
- 進(jìn)程要比線程消耗更多的計算機(jī)資源(采用多列火車相比多個車廂更耗資源)
- 進(jìn)程間不會相互影響,一個線程掛掉將導(dǎo)致整個進(jìn)程掛掉(一列火車不會影響到另外一列火車,但是如果一列火車上中間的一節(jié)車廂著火了,將影響到所有車廂)
- 進(jìn)程可以拓展到多機(jī),進(jìn)程最多適合多核(不同火車可以開在多個軌道上,同一火車的車廂不能在行進(jìn)的不同的軌道上)
- 進(jìn)程使用的內(nèi)存地址可以上鎖,即一個線程使用某些共享內(nèi)存時,其他線程必須等它結(jié)束,才能使用這一塊內(nèi)存。(比如火車上的洗手間)-"互斥鎖"
- 進(jìn)程使用的內(nèi)存地址可以限定使用量(比如火車上的餐廳,最多只允許多少人進(jìn)入,如果滿了需要在門口等,等有人出來了才能進(jìn)去)-“信號量”
應(yīng)用程序如何調(diào)度進(jìn)程和線程
當(dāng)一個應(yīng)用程序啟動時,一個進(jìn)程就被創(chuàng)建了。應(yīng)用程序可能會創(chuàng)建一些線程幫助它完成某些工作,但這不是必須的。操作系統(tǒng)會劃分出一部分內(nèi)存給這個進(jìn)程,當(dāng)前應(yīng)用程序的所有狀態(tài)都將保存在這個私有的內(nèi)存空間中。
當(dāng)你關(guān)閉應(yīng)用時,進(jìn)程也就自動蒸發(fā)掉了,操作系統(tǒng)會將先前被占用的內(nèi)存空間釋放掉。
一個程序并不一定只有一個進(jìn)程,進(jìn)程可以讓操作系統(tǒng)再另起一個進(jìn)程去處理不同的任務(wù)。當(dāng)這種情況發(fā)生時,新的進(jìn)程又將占據(jù)一塊內(nèi)存空間。當(dāng)兩個進(jìn)程需要通信時,它們進(jìn)行進(jìn)程間通訊。
許多應(yīng)用程序都被設(shè)計成以這種方式進(jìn)行工作,所以當(dāng)其中一個進(jìn)程掛掉時,它可以在其他進(jìn)程仍然運行的時候直接重啟。
多進(jìn)程和多線程
理解了上面的內(nèi)容,我們再來重新梳理多進(jìn)程和多線程的概念:
- 多進(jìn)程:多進(jìn)程指的是在同一個時間里,同一個計算機(jī)系統(tǒng)中如果允許兩個或兩個以上的進(jìn)程處于運行狀態(tài)。多進(jìn)程帶來的好處是明顯的,比如你可以聽歌的同時,打開編輯器敲代碼,編輯器和聽歌軟件的進(jìn)程之間絲毫不會相互干擾。
- 多線程是指程序中包含多個執(zhí)行流,即在一個程序中可以同時運行多個不同的線程來執(zhí)行不同的任務(wù),也就是說允許單個程序創(chuàng)建多個并行執(zhí)行的線程來完成各自的任務(wù)。
Chrome 的多進(jìn)程架構(gòu)
由于瀏覽器本身沒有統(tǒng)一的規(guī)范,不同的瀏覽器之間的架構(gòu)可能完全不同,在瀏覽器剛被設(shè)計出來的時候,那時的網(wǎng)頁非常的簡單,每個網(wǎng)頁的資源占有率是非常低的,因此一個進(jìn)程處理多個網(wǎng)頁時可行的。然后在今天,大量網(wǎng)頁變得日益復(fù)雜。把所有網(wǎng)頁都放進(jìn)一個進(jìn)程的瀏覽器面臨在健壯性,響應(yīng)速度,安全性方面的挑戰(zhàn),所以大部分現(xiàn)代瀏覽器都是多進(jìn)程的。
從上面的圖我們可以很明顯的看出 Chrome 是一個多進(jìn)程的架構(gòu),我們打開一個瀏覽器時會啟動多個不同的進(jìn)程協(xié)助瀏覽器將頁面為我們呈現(xiàn)出來:
- 瀏覽器進(jìn)程
- 插件進(jìn)程
- GPU進(jìn)程
- 渲染進(jìn)程
(1) 瀏覽器進(jìn)程
瀏覽器最核心的進(jìn)程,負(fù)責(zé)管理各個標(biāo)簽頁的創(chuàng)建和銷毀、頁面顯示和功能(前進(jìn),后退,收藏等)、網(wǎng)絡(luò)資源的管理,下載等。
(2) 插件進(jìn)程
負(fù)責(zé)每個第三方插件的使用,每個第三方插件使用時候都會創(chuàng)建一個對應(yīng)的進(jìn)程、這可以避免第三方插件crash影響整個瀏覽器、也方便使用沙盒模型隔離插件進(jìn)程,提高瀏覽器穩(wěn)定性。
(3) GPU進(jìn)程
負(fù)責(zé)3D繪制和硬件加速
(4) 渲染進(jìn)程
瀏覽器會為每個窗口分配一個渲染進(jìn)程、也就是我們常說的瀏覽器內(nèi)核,這可以避免單個 page crash 影響整個瀏覽器。
瀏覽器內(nèi)核的多線程
瀏覽器內(nèi)核就是瀏覽器渲染進(jìn)程,從接收下載文件后再到呈現(xiàn)整個頁面的過程,由瀏覽器渲染進(jìn)程負(fù)責(zé)。瀏覽器內(nèi)核是多線程的,在內(nèi)核控制下各線程相互配合以保持同步,一個瀏覽器通常由以下常駐線程組成:
- GUI 渲染線程
- 定時觸發(fā)器線程
- 事件觸發(fā)線程
- 異步http請求線程
- JavaScript 引擎線程
(1) GUI渲染線程
GUI 渲染線程負(fù)責(zé)渲染瀏覽器界面 HTML 元素,當(dāng)界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時,該線程就會執(zhí)行。
(2) 定時觸發(fā)器線程
瀏覽器定時計數(shù)器并不是由 JavaScript 引擎計數(shù)的, 因為 JavaScript 引擎是單線程的, 如果處于阻塞線程狀態(tài)就會影響記計時的準(zhǔn)確, 因此通過單獨線程來計時并觸發(fā)定時是更為合理的方案。
(3) 事件觸發(fā)線程
當(dāng)一個事件被觸發(fā)時該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理。這些事件可以是當(dāng)前執(zhí)行的代碼塊如定時任務(wù)、也可來自瀏覽器內(nèi)核的其他線程如鼠標(biāo)點擊、AJAX異步請求等,但由于JS的單線程關(guān)系所有這些事件都得排隊等待JS引擎處理。
(4) 異步http請求線程
在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求, 將檢測到狀態(tài)變更時,如果設(shè)置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件放到 JavaScript引擎的處理隊列中等待處理。
(5) Javascript引擎線程
Javascript 引擎,也可以稱為JS內(nèi)核,主要負(fù)責(zé)處理 Javascript 腳本程序,例如V8引擎。Javascript 引擎線程理所當(dāng)然是負(fù)責(zé)解析 Javascript 腳本,運行代碼。
由于 JavaScript 是可操縱 DOM 的,如果在修改這些元素屬性同時渲染界面(即 JavaScript 線程和UI線程同時運行),那么渲染線程前后獲得的元素數(shù)據(jù)就可能不一致了。因此為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果,瀏覽器設(shè)置 GUI 渲染線程與 JavaScript 引擎為互斥的關(guān)系,當(dāng) JavaScript 引擎執(zhí)行時 GUI 線程會被掛起, GUI 更新會被保存在一個隊列中等到引擎線程空閑時立即被執(zhí)行。
JavaScript 為何設(shè)計成單線程
從上面我們了解到 JavaScript 的執(zhí)行是單線程的,也就是說,同一個時間只能做一件事。那么,為什么 JavaScript 不設(shè)計成多個線程呢?這樣不是效率更高?
作為瀏覽器腳本語言, JavaScript 的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復(fù)雜的同步問題。比如,假定 JavaScript 同時有兩個線程,一個線程在某個 DOM 節(jié)點上添加內(nèi)容,另一個線程刪除了這個節(jié)點,這時瀏覽器應(yīng)該以哪個線程為準(zhǔn)?
所以,為了避免復(fù)雜性,從一誕生, JavaScript 就是單線程,這已經(jīng)成了這門語言的核心特征,將來也不會改變。
WebWorker 多線程?
Web Worker為Web內(nèi)容在后臺線程中運行腳本提供了一種簡單的方法。線程可以執(zhí)行任務(wù)而不干擾用戶界面:
那么既然 JavaScript 本身被設(shè)計為單線程,為何還會有像 WebWorker 這樣的多線程 API 呢?我們來看一下 WebWorker 的核心特點就明白了:
- 創(chuàng)建 Worker 時, JS 引擎向瀏覽器申請開一個子線程(子線程是瀏覽器開的,完全受主線程控制,而且不能操作DOM)
- JS 引擎線程與 worker 線程間通過特定的方式通信(postMessage API,需要通過序列化對象來與線程交互特定的數(shù)據(jù))
所以 WebWorker 并不違背 JS引擎是單線程的 這一初衷,其主要用途是用來減輕cpu密集型計算類邏輯的負(fù)擔(dān)。
最后
好了,了解完以上知識,再去學(xué)習(xí) JavaScript 的執(zhí)行機(jī)制吧,這些知識會讓你更快深入的理解。





























