第五天 JavaScript單線程詳解

首先,說下爲何 JavaScript 是單線程?

總所周知,JavaScript是以單線程的方式運行的。說到線程就天然聯想到進程。那它們有什麼聯繫呢?javascript

進程和線程都是操做系統的概念。進程是應用程序的執行實例,每個進程都是由私有的虛擬地址空間、代碼、數據和其它系統資源所組成;進程在運行過程當中可以申請建立和使用系統資源(如獨立的內存區域等),這些資源也會隨着進程的終止而被銷燬。而線程則是進程內的一個獨立執行單元,在不一樣的線程之間是能夠共享進程資源的,因此在多線程的狀況下,須要特別注意對臨界資源的訪問控制。在系統建立進程以後就開始啓動執行進程的主線程,而進程的生命週期和這個主線程的生命週期一致,主線程的退出也就意味着進程的終止和銷燬。主線程是由系統進程所建立的,同時用戶也能夠自主建立其它線程,這一系列的線程都會併發地運行於同一個進程中。html

顯然,在多線程操做下能夠實現應用的 並行處理 ,從而以更高的CPU利用率提升整個應用程序的性能和吞吐量。特別是如今不少語言都支持多核並行處理技術,然而JavaScript卻以單線程執行,爲何呢?html5

其實這與它的用途有關。做爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操做DOM。若以多線程的方式操做這些DOM,則可能出現操做的衝突。假設有兩個線程同時操做一個DOM元素,線程1要求瀏覽器刪除DOM,而線程2卻要求修改DOM樣式,這時瀏覽器就沒法決定採用哪一個線程的操做。固然,咱們能夠爲瀏覽器引入「鎖」的機制來解決這些衝突,但這會大大提升複雜性,因此 JavaScript 從誕生開始就選擇了單線程執行。java

另外,由於 JavaScript 是單線程的,在某一時刻內只能執行特定的一個任務,而且會阻塞其它任務執行。那麼對於相似I/O等耗時的任務,就不必等待他們執行完後才繼續後面的操做。在這些任務完成前,JavaScript徹底能夠往下執行其餘操做,當這些耗時的任務完成後則以回調的方式執行相應處理。這些就是JavaScript與生俱來的特性:異步與回調。git

固然對於不可避免的耗時操做(如:繁重的運算,多重循環),HTML5提出了 Web Worker ,它會在當前JavaScript的執行主線程中利用Worker類新開闢一個額外的線程來加載和運行特定的JavaScript文件,這個新的線程和JavaScript的主線程之間並不會互相影響和阻塞執行,並且在Web Worker中提供了這個新線程和JavaScript主線程之間數據交換的接口:postMessage和onMessage事件。但在HTML5 Web Worker中是不能操做DOM的,任何須要操做DOM的任務都須要委託給JavaScript主線程來執行,因此雖然引入HTML5 Web Worker,但仍然沒有改線JavaScript單線程的本質。web

併發模式與Event Loop

JavaScript 有個基於「Event Loop」併發的模型。
啊,併發?不是說 JavaScript是單線程嗎? 沒錯,的確是單線程,可是併發與並行是有區別的。
前者是邏輯上的同時發生,然後者是物理上的同時發生。因此,單核處理器也能實現併發。瀏覽器

圖片描述

併發與並行多線程

並行你們都好理解,而 所謂「併發」是指兩個或兩個以上的事件在同一時間間隔中發生。 如上圖的第一個表,因爲計算機系統只有一個CPU,故ABC三個程序從「微觀」上是交替使用CPU,但交替時間很短,用戶察覺不到,造成了「宏觀」意義上的併發操做。併發

Runtime 概念

下面的內容解釋一個理論上的模型。現代 JavaScript 引擎已着重實現和優化了如下所描述的幾個概念。異步

圖片描述

Stack(棧)

這裏放着JavaScript正在執行的任務。每一個任務被稱爲幀(stack of frames)。

function f(b){
  var a = 12;
  return a+b+35;
}

function g(x){
  var m = 4;
  return f(m*x);
}

g(21);

上述代碼調用 g 時,建立棧的第一幀,該幀包含了 g 的參數和局部變量。當 g 調用 f 時,第二幀就會被建立,而且置於第一幀之上,固然,該幀也包含了 f 的參數和局部變量。當 f 返回時,其對應的幀就會出棧。同理,當 g 返回時,棧就爲空了( 棧的特定就是後進先出 Last-in first-out (LIFO))。

Heap(堆)

一個用來表示內存中一大片非結構化區域的名字,對象都被分配在這。

Queue(隊列)

一個 JavaScript runtime 包含了一個任務隊列,該隊列是由一系列待處理的任務組成。而每一個任務都有相對應的函數。當棧爲空時,就會從任務隊列中取出一個任務,並處理之。該處理會調用與該任務相關聯的一系列函數(所以會建立一個初始棧幀)。當該任務處理完畢後,棧就會再次爲空。 (Queue的特色是先進先出 First-in First-out (FIFO))。

爲了方便描述與理解,做出如下約定:

  • Stack棧爲 主線程

  • Queue隊列爲 任務隊列(等待調度到主線程執行)

OK,上述知識點幫助咱們理清了一個 JavaScript runtime 的相關概念,這有助於接下來的分析。

Event Loop

之因此被稱爲Event loop,是由於它以如下相似方式實現:

while(queue.waitForMessage()){
  queue.processNextMessage();
}

正如上述所說,「任務隊列」是一個事件的隊列,若是I/O設備完成任務或用戶觸發事件(該事件指定了回調函數),那麼相關事件處理函數就會進入「任務隊列」,當主線程空閒時,就會調度「任務隊列」裏第一個待處理任務,(FIFO)。固然,對於定時器,當到達其指定時間時,纔會把相應任務插到「任務隊列」尾部。

「執行至完成」

每當某個任務執行完後,其它任務纔會被執行。也就是說,當一個函數運行時,它不能被取代且會在其它代碼運行前先完成。
固然,這也是Event Loop的一個 缺點 :當一個任務完成時間過長,那麼應用就不能及時處理用戶的交互(如點擊事件),甚至致使該應用奔潰。一個比較好解決方案是:將任務完成時間縮短,或者儘量將一個任務分紅多個任務執行。

毫不阻塞

JavaScript與其它語言不一樣,其Event Loop的一個特性是永不阻塞。I/O操做一般是經過事件和回調函數處理。因此,當應用等待 indexedDB 或 XHR 異步請求返回時,其仍能處理其它操做(如用戶輸入)。

例外是存在的,如alert或者同步XHR,但避免它們被認爲是最佳實踐。注意的是,例外的例外也是存在的(但一般是實現錯誤而非其它緣由)。

定時器

定時器的一些概念

上面也提到,在到達指定時間時,定時器就會將相應回調函數插入「任務隊列」尾部。這就是「定時器(timer)」功能。

定時器 包括setTimeout與setInterval兩個方法。它們的第二個參數是指定其回調函數推遲每隔多少毫秒數後執行。
對於第二個參數有如下須要注意的地方:

  • 當第二個參數缺省時,默認爲0;

  • 當指定的值小於4毫秒,則增長到4ms(4ms是HTML5標準指定的,對於2010年及以前的瀏覽器則是10ms);

若是你理解上述知識,那麼如下代碼就應該對你沒什麼問題了:

console.log(1);
setTimeout(function(){
    console.log(2);
},10);
console.log(3);
// 輸出:1 3 2

深刻了解定時器 零延遲 setTimeout(func, 0)

零延遲並非意味着回調函數馬上執行。它取決於主線程當前是否空閒與「任務隊列」裏其前面正在等待的任務。

看看如下代碼:

(function () {

  console.log('this is the start');

  setTimeout(function cb() {
    console.log('this is a msg from call back');
  });

  console.log('this is just a message');

  setTimeout(function cb1() {
    console.log('this is a msg from call back1');
  }, 0);

  console.log('this is the  end');

})();

// 輸出以下:
this is the start
this is just a message
this is the end
undefined // 當即調用函數的返回值
this is a msg from callback
this is a msg from a callback1

setTimeout(func, 0)的做用

  • 讓瀏覽器渲染當前的變化(不少瀏覽器UI render和js執行是放在一個線程中,線程阻塞會致使界面沒法更新渲染)

  • 從新評估」scriptis running too long」警告

  • 改變執行順序

再看看如下代碼:

<button id='do'> Do long calc!</button>
<div id='status'></div>
<div id='result'></div>
 
 
$('#do').on('click', function(){
  
  $('#status').text('calculating....');// 此處會觸發redraw事件,但會放到隊列裏執行,直到long()執行完。
  
  // 沒設定定時器,用戶將沒法看到「calculating...」
  long();// 執行長時間任務,形成阻塞
   
  // 設定了定時器,用戶就如期看到「calculating...」
  //setTimeout(long,50);// 大約50ms後,將耗時長的long回調函數插入「任務隊列」末尾,根據先進先出原則,其將在redraw以後被調度到主線程執行
  
 });
  
function long(){
  var result = 0
  for (var i = 0; i<1000; i++){
    for (var j = 0; j<1000; j++){
      for (var k = 0; k<1000; k++){
        result = result + i+j+k
      }
    } 
  }
  $('#status').text('calclation done'); // 在本案例中,該語句必須放到這裏,這將使它與回調函數的行爲相似
}

正版與翻版setInterval的區別

你們均可能知道經過setTimeout能夠模仿setInterval的效果,下面咱們看看如下代碼的區別:

// 利用setTimeout模仿setInterval
setTimeout(function(){
    /* 執行一些操做. */
    setTimeout(arguments.callee, 10);
}, 1000);

setInterval(function(){
    /* 執行一些操做 */
}, 1000);

可能你認爲這沒什麼區別。的確,當回調函數裏的操做耗時很短時,並不能看出它們有什麼區別。
其實:上面案例中的 setTimeout 老是會在其回調函數執行後延遲 10ms(或者更多,但不可能少)再次執行回調函數,從而實現setInterval的效果,而 setInterval 老是 10ms 執行一次,而無論它的回調函數執行多久。

因此,若是 setInterval 的回調函數執行時間比你指定的間隔時間相等或者更長,那麼其回調函數會連在一塊兒執行。

你能夠試試運行如下代碼:

var counter = 0;
var initTime = new Date().getTime();
var timer = setInterval(function(){
    if(counter===2){
        clearInterval(timer);
    }
    if(counter === 0){
        for(var i = 0; i < 1990000000; i++){
            ;
        }
    }

    console.log("第"+counter+"次:" + (new Date().getTime() - initTime) + " ms");

    counter++;
},1000);

我電腦Chrome瀏覽器的輸入以下:

第0次:2007 ms
第1次:2013 ms
第2次:3008 ms

瀏覽器

瀏覽器不是單線程的

上面說了這麼多關於JavaScript是單線程的,下面說說其宿主環境——瀏覽器。
瀏覽器的內核是多線程 的,它們在內核制控下相互配合以保持同步,一個瀏覽器至少實現三個常駐線程:

  1. javascript引擎線程 javascript引擎是基於事件驅動單線程執行的,JS引擎一直等待着任務隊列中任務的到來,而後加以處理,瀏覽器不管何時都只有一個JS線程在運行JS程序。

  2. GUI渲染線程 GUI渲染線程負責渲染瀏覽器界面,當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時,該線程就會執行。但須要注意GUI渲染線程與JS引擎是互斥的,當JS引擎執行時GUI線程會被掛起,GUI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行。

  3. 瀏覽器事件觸發線程 事件觸發線程,當一個事件被觸發時該線程會把事件添加到「任務隊列」的隊尾,等待JS引擎的處理。這些事件可來自JavaScript引擎當前執行的代碼塊如setTimeOut、也可來自瀏覽器內核的其餘線程如鼠標點擊、AJAX異步請求等,但因爲JS是單線程執行的,全部這些事件都得排隊等待JS引擎處理。

在Chrome瀏覽器中,爲了防止因一個標籤頁奔潰而影響整個瀏覽器,其每一個標籤頁都是一個 進程 。固然,對於同一域名下的標籤頁是可以相互通信的,具體可看 瀏覽器跨標籤通信 。在Chrome設計中存在不少的進程,並利用進程間通信來完成它們之間的同步,所以這也是Chrome快速的法寶之一。對於Ajax的請求也須要特殊線程來執行,當須要發送一個Ajax請求時,瀏覽器會開闢一個新的線程來執行HTTP的請求,它並不會阻塞JavaScript線程的執行,當HTTP請求狀態變動時,相應事件會被做爲回調放入到「任務隊列」中等待被執行。

看看如下代碼:

document.onclick = function(){
    console.log("click")
}

for(var i = 0; i< 100000000; i++);

解釋一下代碼:首先向document註冊了一個click事件,而後就執行了一段耗時的for循環,在這段for循環結束前,你能夠嘗試點擊頁面。當耗時操做結束後,console控制檯就會輸出以前點擊事件的"click"語句。這視乎證實了點擊事件(也包括其它各類事件)是由額外單獨的線程觸發的,事件觸發後就會將回調函數放進了「任務隊列」的末尾,等待着JavaScript主線程的執行。

總結

  • JavaScript是單線程的,同一時刻只能執行特定的任務。而瀏覽器是多線程的。

  • 異步任務(各類瀏覽器事件、定時器等)都是先添加到「任務隊列」(定時器則到達其指定參數時)。當Stack棧(JS主線程)爲空時,就會讀取Queue隊列(任務隊列)的第一個任務(隊首),而後執行。

JavaScript爲了不復雜性,而實現單線程執行。而今JavaScript卻變得愈來愈不簡單了,固然這也是JavaScript迷人的地方。

參考資料

  1. JavaScript運行機制詳解:再談Event Loop

  2. JavaScript單線程和瀏覽器事件循環

  3. JavaScript單線程深刻分析

  4. 也談setTimeout

  5. 單線程的JavaScript

原文連接: http://www.codeceo.com/article/javascript-threaded.html原文做者:碼農網 – 劉健超[ 原創做品,轉載必須在正文中標註並保留原文連接和做者等信息。]

相關文章
相關標籤/搜索