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