1. 程式人生 > >瀏覽器的多執行緒與js引擎的單執行緒

瀏覽器的多執行緒與js引擎的單執行緒

1. 瀏覽器的執行緒與程序

(1) 程序與執行緒

程序

學術上說,程序是一個具有一定獨立功能的程式在一個數據集上的一次動態執行的過程,是作業系統進行資源分配和排程的一個獨立單位,是應用程式執行的載體。我們這裡將程序比喻為工廠的車間,它代表CPU所能處理的單個任務。任一時刻,CPU總是執行一個程序,其他程序處於非執行狀態。

執行緒

在早期的作業系統中並沒有執行緒的概念,程序是能擁有資源和獨立執行的最小單位,也是程式執行的最小單位。任務排程採用的是時間片輪轉的搶佔式排程方式,而程序是任務排程的最小單位,每個程序有各自獨立的一塊記憶體,使得各個程序之間記憶體地址相互隔離。後來,隨著計算機的發展,對CPU的要求越來越高,程序之間的切換開銷較大,已經無法滿足越來越複雜的程式的要求了。於是就發明了執行緒,執行緒是程式執行中一個單一的順序控制流程,是程式執行流的最小單元。這裡把執行緒比喻一個車間的工人,即一個車間可以允許由多個工人協同完成一個任務。

程序和執行緒的區別和關係

程序是作業系統分配資源的最小單位,執行緒是程式執行的最小單位。
一個程序由一個或多個執行緒組成,執行緒是一個程序中程式碼的不同執行路線;
程序之間相互獨立,但同一程序下的各個執行緒之間共享程式的記憶體空間(包括程式碼段、資料集、堆等)及一些程序級的資源(如開啟檔案和訊號)。
排程和切換:執行緒上下文切換比程序上下文切換要快得多。

多程序和多執行緒

多程序:多程序指的是在同一個時間裡,同一個計算機系統中如果允許兩個或兩個以上的程序處於執行狀態。多程序帶來的好處是明顯的,比如你可以聽歌的同時,開啟編輯器敲程式碼,編輯器和聽歌軟體的程序之間絲毫不會相互干擾。
多執行緒是指程式中包含多個執行流,即在一個程式中可以同時執行多個不同的執行緒來執行不同的任務,也就是說允許單個程式建立多個並行執行的執行緒來完成各自的任務。

(2) 瀏覽器的程序與執行緒

首先開啟瀏覽器,然後開啟shift + Esc開啟chrome的工作管理員

此時只有三個程序:
瀏覽器程序(Browser程序): 瀏覽器的主程序(負責協調、主控),只有一個。作用有
    負責瀏覽器介面顯示,與使用者互動。如前進,後退等
    負責各個頁面的管理,建立和銷燬其他程序
    將Renderer程序得到的記憶體中的Bitmap,繪製到使用者介面上
    網路資源的管理,下載等
GPU程序:用於3D繪製等(可禁止掉,而且這個與頁面渲染過程的Composite Layers 有關係,後面效能優化相關文章學習到再來研究一下GPU)
瀏覽器渲染程序(Renderer程序,內部是多執行緒的)
每一個標籤頁的開啟都會建立一個瀏覽器渲染程序(瀏覽器核心)。預設每個Tab頁面一個程序,互不影響。主要作用為頁面渲染,指令碼執行,事件處理等

2. 瀏覽器為什麼要多程序?

在瀏覽器剛被設計出來的時候,那時的網頁非常的簡單,每個網頁的資源佔有率是非常低的,因此一個程序處理多個網頁時可行的。然後在今天,大量網頁變得日益複雜。把所有網頁都放進一個程序的瀏覽器面臨在健壯性,響應速度,安全性方面的挑戰。因為如果瀏覽器中的一個tab網頁崩潰的話,將會導致其他被開啟的網頁應用。另外相對於執行緒,程序之間是不共享資源和地址空間的,所以不會存在太多的安全問題,而由於多個執行緒共享著相同的地址空間和資源,所以會存線上程之間有可能會惡意修改或者獲取非授權資料等複雜的安全問題。

3. Browser程序與Render程序,GPU程序之間的如何合作?

在How browser work為文章中看到過這樣一幅圖:



這裡的Browser engine我想對應的就是Browser程序,Rendering engine對應的就是Render程序。
針對與使用者開啟一個標籤頁,可以看到首先控制的還是Browser程序。然後我們再看一下chromium多執行緒模型:



基本工作方式如下
Browser程序收到使用者的請求,首先由UI執行緒處理,而且將相應的任務轉給IO執行緒,他隨機將該任務傳遞給Render程序;
Render程序的IO執行緒經過簡單解釋後交給渲染執行緒,渲染執行緒接收請求,載入網頁並渲染網頁,這其中可能需要Browser程序獲取資源和需要GPU程序來幫助渲染,最後Render程序將結果由IO執行緒傳遞給Browser程序;
Browser程序接收到結果並將結果繪製出來;

4. 覽器渲染Render程序(瀏覽器核心)有哪些執行緒?

GUI渲染執行緒

負責渲染瀏覽器介面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。
當介面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該執行緒就會執行
注意,GUI渲染執行緒與JS引擎執行緒是互斥的,當JS引擎執行時GUI執行緒會被掛起(相當於被凍結了),GUI更新會被儲存在一個佇列中等到JS引擎空閒時立即被執行。

JS引擎執行緒

也稱為JS核心,負責處理Javascript指令碼程式。(例如V8引擎)
JS引擎執行緒負責解析Javascript指令碼,執行程式碼。
JS引擎一直等待著任務佇列中任務的到來,然後加以處理,一個Tab頁(renderer程序)中無論什麼時候都只有一個JS執行緒在執行JS程式
同樣注意,GUI渲染執行緒與JS引擎執行緒是互斥的,所以如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞。

事件觸發執行緒

歸屬於瀏覽器而不是JS引擎,用來控制事件迴圈(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開執行緒協助)
當JS引擎執行程式碼塊如setTimeOut時(也可來自瀏覽器核心的其他執行緒,如滑鼠點選、AJAX非同步請求等),會將對應任務新增到事件執行緒中
當對應的事件符合觸發條件被觸發時,該執行緒會把事件新增到待處理佇列的隊尾,等待JS引擎的處理
注意,由於JS的單執行緒關係,所以這些待處理佇列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時才會去執行)

定時觸發器執行緒

傳說中的setInterval與setTimeout所線上程
瀏覽器定時計數器並不是由JavaScript引擎計數的,(因為JavaScript引擎是單執行緒的, 如果處於阻塞執行緒狀態就會影響記計時的準確)
因此通過單獨執行緒來計時並觸發定時(計時完畢後,新增到事件佇列中,等待JS引擎空閒後執行)
注意,W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算為4ms。

非同步http請求執行緒

在XMLHttpRequest在連線後是通過瀏覽器新開一個執行緒請求
將檢測到狀態變更時,如果設定有回撥函式,非同步執行緒就產生狀態變更事件,將這個回撥再放入事件佇列中。再由JavaScript引擎執行。

5. JS引擎執行緒的相關介紹

(1) 為什麼JavaScript是單執行緒?

首先在明確一個概念,JS引擎執行緒生存在Render程序(瀏覽器渲染程序)。其實從前面的程序,執行緒之間的介紹已經明白,執行緒之間資源共享,相互影響。假設javascript的執行存在兩個執行緒,彼此操作了同一個資源,這樣會造成同步問題,修改到底以誰為標準。
所以,JavaScript就是單執行緒,這已經成了這門語言的核心特徵,將來也不會改變。

(2) WebWorker會造成js多執行緒嗎?

//index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webWorker</title>
</head>
<body>
  <div>
    <a href='./webworker/index.html'>web worker</a>
  </div>
</body>
</html>
//worker.js
onmessage = function (count) {
  console.log("web worker start")

    var i = 0;

    while(i < count.data) {
      i ++;
    }

    postMessage("web worker finish");
};
然後開啟performence面板檢視:

圖中藍色框是瀏覽器下載完wokder.js檔案。緊接著我們我們可以看到紅色框DedicatedWorker Thread,在workder thread的時間內去執行worker.js,然後將計算好的結果返回給main thread,最後執行到藍色框中去。

這樣看起來好像是建立了一個新的執行緒。如果我沒有使用web worker的情況下,DedicatedWorker Thread根本就不存在。
這好像看起來並沒有什麼說服性。我們再看一個例子,在本地檢視這個例子的時候,具體的數字是我調的,是想達到下面圖示的效果。

//index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webWorker</title>
</head>
<body>
    <script>
        var worker = new Worker("worker.js");
        worker.postMessage(123456);


        worker.onmessage = function (e) {
            console.log(e.data)
        };

        setTimeout(function() {
            var i = 0;

            while(i < 9234156) {
                i ++;
            }
        }, 180);
    </script>
</body>
</html>
//worker.js
onmessage = function (count) {
  console.log("web worker start")

    var i = 0;

    while(i < count.data) {
      i ++;
    }

    postMessage("web worker finish");
};

如圖所示,setTimeout的程式碼執行的時候,worker.js的程式碼也在執行。這裡我得到的結果是webworker可以在js引擎執行程式碼的時候去執行另外的程式碼。

這裡我們需要在明確開始的問題了,是造成js執行的多執行緒還是js引擎的多執行緒。這裡是兩個概念。上面的圖示中main的欄目中是瀏覽器渲染Render程序(本人猜測),因為在這個過程中我們可以看到js程式碼的執行,也有GUI渲染執行緒進行html程式碼的解析。現在我看到的是DedicatedWorker Thread是與覽器渲染Render程序同一級的(當然這些都是performence展現出來給我們看的)。

我在MDN上看到一句話:Worker介面會生成真正的作業系統級別的執行緒。所以這裡的webworker不是一個新的js引擎執行緒。而是作業系統級別的執行緒。執行緒的執行不會影響到原有的js引擎的執行,也不會影響到瀏覽器渲染Render程序。至於其內部實現,本人就望塵莫及了。但是,人家webworker確實實現了js程式碼執行的多執行緒(當然這些都是本人的基於看到的結果猜測的,沒有找到實際的論證資料,如果有知道的可以告知,謝謝了)。

所以我目前得到的結論是: webworker是可以造成js程式碼的多執行緒執行,但不是js引擎多執行緒的執行。webwoker的生命週期是由js引擎執行緒控制的,因為webweoker提供了一系列的api供我們操作。

然後我們再說一下webweoker中的一些不能操作的內容:也是出於安全考慮,如果不太小心,那麼併發(concurrency)會對你的程式碼產生有趣的影響。然而,對於 web worker 來說,與其他執行緒的通訊點會被很小心的控制,這意味著你很難引起併發問題。所以webworker也自己做了限制(下面的內容是在網上找到的,因為我沒有這麼使用過webworker):

1、不能訪問DOM和BOM物件的,Location和navigator的只讀訪問,並且navigator封裝成了WorkerNavigator物件,更改部分屬性。無法讀取本地檔案系統

2、子執行緒和父級執行緒的通訊是通過值拷貝,子執行緒對通訊內容的修改,不會影響到主執行緒。在通訊過程中值過大也會影響到效能(解決這個問題可以用transferable objects)

3、並非真的多執行緒,多執行緒是因為瀏覽器的功能

4、相容性

5 因為執行緒是通過importScripts引入外部的js,並且直接執行,其實是不安全的,很容易被外部注入一些惡意程式碼

6、條數限制,大多瀏覽器能建立webworker執行緒的條數是有限制的,雖然可以手動去拓展,但是如果不設定的話,基本上都在20條以內,每條執行緒大概5M左右,需要手動關掉一些不用的執行緒才能夠建立新的執行緒(相關解決方案)

7、js存在真的執行緒的東西,比如SharedArrayBuffer

(3) js程式碼的執行(Event Loop)與其他執行緒之間的合作

JavaScript 引擎並不是獨立執行的,它執行在宿主環境中,對多數開發者來說通常就是Web 瀏覽器。提供了一種機制來處理程式中多個塊(這裡的塊可以理解成多個回掉函式)的執行,且執行每塊時呼叫JavaScript 引擎,這種機制被稱為事件迴圈。換句話說,JavaScript 引擎本身並沒有時間的概念,只是一個按需執行JavaScript 任意程式碼片段的環境。“事件”(JavaScript 程式碼執行)排程總是由包含它的環境進行。這個排程是由事件觸發執行緒排程的。

舉例來說,如果你的JavaScript 程式發出一個Ajax 請求,從伺服器獲取一些資料,那你就在一個函式(通常稱為回撥函式)中設定好響應程式碼,然後JavaScript 引擎會通知宿主環境(事件觸發執行緒):“嘿,現在我要暫停執行,你一旦完成網路請求,拿到了資料,就請呼叫這個函式。”然後瀏覽器就會設定偵聽來自網路的響應,拿到要給你的資料之後,就會把回撥函式插入到事件迴圈,以此實現對這個回撥的排程執行。

請看下圖對於一個頁面的請求以及js的執行過程中,上面的程序/執行緒之間的合作。

6. Promise的出現

其實關於Promise的內容我之前也有看過,具體內容可以檢視文章現在這裡我只是將Promise基於任務佇列的內容用程式碼的形式展示出來。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webWorker</title>
</head>
<body>
    <script>
      setTimeout(function() {
        console.log('setTimeout run');
      }, 0);

        new Promise(function(resolve, reject) {
        resolve();
        })
        .then(function(){  
        console.log('promise run');
    } );  
    </script>
</body>
</html>
這裡的輸出結果相信大家猜測的出來:
promise run
setTimeout run

這裡的原因不在多說,因為首先js引擎要先執行主執行緒js的程式碼(會先執行完,因為一個js的載入,從上往下會執行完成之後,js引擎才會有時間去從事件迴圈佇列中拿出程式碼塊執行),至於setTimeout,間隔時間儘管為0ms,其實真正執行的時候是4ms。而且回掉函式是放在事件迴圈佇列裡的。那麼Promise呢?好比我們現在執行的是js主執行緒,執行完成之後,js引擎不會立即去事件迴圈佇列裡取程式碼塊執行,而是說當前主執行緒還有一點事情沒有做完,那就是promise,在之前的文章中也談過。二者事件的粒度不同,promsie是事件迴圈佇列之上的任務佇列。

參考:
https://blog.csdn.net/Steward2011/article/details/51319298
https://segmentfault.com/a/1190000012925872
http://www.imweb.io/topic/58e3bfa845e5c13468f567d5
http://www.ruanyifeng.com/blog/2014/10/event-loop.html#comment-text
https://segmentfault.com/a/1190000009313491