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