摘要:標簽單線程首發地址碼農網細說單線程的一些事最近被同學問道單線程的一些事,我竟回答不上。若以多線程的方式操作這些,則可能出現操作的沖突。另外,因為是單線程的,在某一時刻內只能執行特定的一個任務,并且會阻塞其它任務執行。
標簽: JavaScript 單線程
首發地址:碼農網《細說JavaScript單線程的一些事》
最近被同學問道 JavaScript 單線程的一些事,我竟回答不上。好吧,感覺自己的 JavaScript 白學了。下面是我這幾天整理的一些關于 JavaScript 單線程的一些事。
首先,說下為什么 JavaScript 是單線程?總所周知,JavaScript 是以單線程的方式運行的。說到線程就自然聯想到進程。那它們有什么聯系呢?
進程和線程都是操作系統的概念。進程是應用程序的執行實例,每一個進程都是由私有的虛擬地址空間、代碼、數據和其它系統資源所組成;進程在運行過程中能夠申請創建和使用系統資源(如獨立的內存區域等),這些資源也會隨著進程的終止而被銷毀。而線程則是進程內的一個獨立執行單元,在不同的線程之間是可以共享進程資源的,所以在多線程的情況下,需要特別注意對臨界資源的訪問控制。在系統創建進程之后就開始啟動執行進程的主線程,而進程的生命周期和這個主線程的生命周期一致,主線程的退出也就意味著進程的終止和銷毀。主線程是由系統進程所創建的,同時用戶也可以自主創建其它線程,這一系列的線程都會并發地運行于同一個進程中。
顯然,在多線程操作下可以實現應用的并行處理,從而以更高的 CPU 利用率提高整個應用程序的性能和吞吐量。特別是現在很多語言都支持多核并行處理技術,然而 JavaScript 卻以單線程執行,為什么呢?
其實這與它的用途有關。作為瀏覽器腳本語言,JavaScript 的主要用途是與用戶互動,以及操作 DOM。若以多線程的方式操作這些 DOM,則可能出現操作的沖突。假設有兩個線程同時操作一個 DOM 元素,線程 1 要求瀏覽器刪除 DOM,而線程 2 卻要求修改 DOM 樣式,這時瀏覽器就無法決定采用哪個線程的操作。當然,我們可以為瀏覽器引入“鎖”的機制來解決這些沖突,但這會大大提高復雜性,所以 JavaScript 從誕生開始就選擇了單線程執行。
另外,因為 JavaScript 是單線程的,在某一時刻內只能執行特定的一個任務,并且會阻塞其它任務執行。那么對于類似 I/O 等耗時的任務,就沒必要等待他們執行完后才繼續后面的操作。在這些任務完成前,JavaScript 完全可以往下執行其他操作,當這些耗時的任務完成后則以回調的方式執行相應處理。這些就是 JavaScript 與生俱來的特性:異步與回調。
當然對于不可避免的耗時操作(如:繁重的運算,多重循環),HTML5 提出了Web Worker,它會在當前 JavaScript 的執行主線程中利用 Worker 類新開辟一個額外的線程來加載和運行特定的 JavaScript 文件,這個新的線程和 JavaScript 的主線程之間并不會互相影響和阻塞執行,而且在 Web Worker 中提供了這個新線程和 JavaScript 主線程之間數據交換的接口:postMessage 和 onMessage 事件。但在 HTML5 Web Worker 中是不能操作 DOM 的,任何需要操作 DOM 的任務都需要委托給 JavaScript 主線程來執行,所以雖然引入 HTML5 Web Worker,但仍然沒有改線 JavaScript 單線程的本質。
并發模式與 Event LoopJavaScript 有個基于“Event Loop”并發的模型。
啊,并發?不是說 JavaScript 是單線程嗎? 沒錯,的確是單線程,但是并發與并行是有區別的。
前者是邏輯上的同時發生,而后者是物理上的同時發生。所以,單核處理器也能實現并發。
并發與并行
并行大家都好理解,而所謂“并發”是指兩個或兩個以上的事件在同一時間間隔中發生。如上圖的第一個表,由于計算機系統只有一個 CPU,故 ABC 三個程序從“微觀”上是交替使用 CPU,但交替時間很短,用戶察覺不到,形成了“宏觀”意義上的并發操作。
Runtime 概念下面的內容解釋一個理論上的模型?,F代 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深入了解定時器
零延遲并不是意味著回調函數立刻執行。它取決于主線程當前是否空閑與“任務隊列”里其前面正在等待的任務。
看看以下代碼:
(function () { console.log("this is the start"); setTimeout(function cb() { console.log("this is a msg from callback"); }); console.log("this is just a message"); setTimeout(function cb1() { console.log("this is a msg from callback1"); }, 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 callback1
讓瀏覽器渲染當前的元素更改(瀏覽器將 UI render 和 JavaScript 的執行是放在一個線程中,線程阻塞會導致界面無法更新渲染)
重新評估“scriptis running too long”警告
改變執行順序
再看看以下代碼:
$("#do").on("click", function() { // 此處會觸發 redraw 事件,但會放到隊列里執行,直到 long() 執行完。 $("#status").text("calculating...."); // 沒設定定時器,用戶將無法看到 “calculating...” // 這是因為“calculation”的 redraw 事件會緊接在 // “calculating...”的 redraw 事件后執行 long(); // 執行長時間任務,造成阻塞 // 設定了定時器,用戶就如期看到“calculating...” // 大約 50ms 后,將耗時長的 long 回調函數插入“任務隊列”末尾, // 根據先進先出原則,其將在 redraw 之后被調度到主線程執行 //setTimeout(long,50); }); 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("calculation done"); }
大家都可能知道通過 setTimeout 可以模仿 setInterval 的效果,下面我們看看以下代碼的區別:
// 利用 setTimeout 模仿 setInterval setTimeout(function() { /* 執行一些操作. */ setTimeout(arguments.callee, 1000); }, 1000); setInterval(function() { /* 執行一些操作 */ }, 1000);
可能你認為這沒什么區別。的確,當回調函數里的操作耗時很短時,并不能看出它們有什么區別。
其實:上面案例中的 setTimeout 總是會在其回調函數執行后延遲 1000ms(或者更多,但不可能少)再次執行回調函數,從而實現 setInterval 的效果,而 setInterval 總是 1000ms 執行一次,而不管它的回調函數執行多久。
所以,如果 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
從上面的執行結果可看出,第一次和第二次執行間隔很短(不足 1000ms)。
瀏覽器 瀏覽器不是單線程的上面說了這么多關于 JavaScript 是單線程的,下面說說其宿主環境——瀏覽器。
瀏覽器的內核是多線程的,它們在內核制控下相互配合以保持同步,一個瀏覽器至少實現三個常駐線程:
JavaScript 引擎線程 JavaScript 引擎是基于事件驅動單線程執行的,JavaScript 引擎一直等待著任務隊列中任務的到來,然后加以處理。
GUI 渲染線程 GUI 渲染線程負責渲染瀏覽器界面,當界面需要重繪(Repaint)或由于某種操作引發回流(reflow)時,該線程就會執行。但需要注意 GUI 渲染線程與 JavaScript 引擎是互斥的,當 JavaScript 引擎執行時 GUI 線程會被掛起,GUI 更新會被保存在一個隊列中等到 JavaScript 引擎空閑時立即被執行。
瀏覽器事件觸發線程事件觸發線程,當一個事件被觸發時該線程會把事件添加到“任務隊列”的隊尾,等待 JavaScript 引擎的處理。這些事件可來自 JavaScript 引擎當前執行的代碼塊如 setTimeOut、也可來自瀏覽器內核的其他線程如鼠標點擊、AJAX 異步請求等,但由于 JavaScript 是單線程執行的,所有這些事件都得排隊等待 JavaScript 引擎處理。
在 Chrome 瀏覽器中,為了防止因一個標簽頁奔潰而影響整個瀏覽器,其每個標簽頁都是一個進程(Renderer Process)。當然,對于同一域名下的標簽頁是能夠相互通訊的,具體可看 瀏覽器跨標簽通訊。在 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 棧(JavaScript 主線程)為空時,就會讀取 Queue 隊列(任務隊列)的第一個任務(隊首),然后執行。
JavaScript 為了避免復雜性,而實現單線程執行。而如今 JavaScript 卻變得越來越不簡單了,當然這也是 JavaScript 迷人的地方。
后續更新(回復網友的問題)關于"setTimeout(func, 0)的作用"一節中,redraw事件發生后,事件處理函數被插入任務隊列,等待當前棧中long函數執行完畢再執行。此時通過setTimeout(long,0)即可將long函數插到任務隊列中redraw事件處理函數的后面。事實上Chrome中也確實是這么處理的(我的版本號是55.0.2883.87 m),可是最新的火狐和Edge都至少要將延時設置為15ms以上,請問這是為什么?
答:恩,這的確取決于瀏覽器的內部實現。
昨晚,我看了Chrome(chromium)的定時器源碼實現:
一些變量的定義:
static const int maxIntervalForUserGestureForwarding = 1000; // One second matches Gecko. static const int maxTimerNestingLevel = 5; static const double oneMillisecond = 0.001; // Chromium uses a minimum timer interval of 4ms. We"d like to go // lower; however, there are poorly coded websites out there which do // create CPU-spinning loops. Using 4ms prevents the CPU from // spinning too busily and provides a balance between CPU spinning and // the smallest possible interval timer. static const double minimumInterval = 0.004;
定時器的部分實現:
DOMTimer::DOMTimer(ExecutionContext* context, ScheduledAction* action, int interval, bool singleShot, int timeoutID) : SuspendableTimer(context) , m_timeoutID(timeoutID) , m_nestingLevel(context->timers()->timerNestingLevel() + 1) , m_action(action) { ASSERT(timeoutID > 0); if (shouldForwardUserGesture(interval, m_nestingLevel)) m_userGestureToken = UserGestureIndicator::currentToken(); InspectorInstrumentation::asyncTaskScheduled(context, singleShot ? "setTimeout" : "setInterval", this, !singleShot); double intervalMilliseconds = std::max(oneMillisecond, interval * oneMillisecond); if (intervalMilliseconds < minimumInterval && m_nestingLevel >= maxTimerNestingLevel) intervalMilliseconds = minimumInterval; if (singleShot) startOneShot(intervalMilliseconds, BLINK_FROM_HERE); else startRepeating(intervalMilliseconds, BLINK_FROM_HERE); }
從上述代碼可看出:Chrome 實現的定時器的最小時間間隔是 1ms。只有滿足 intervalMilliseconds < minimumInterval && m_nestingLevel >= maxTimerNestingLevel 該條件時,定時器的最小時間間隔才是 4ms。
因此,各瀏覽器是往響應更快的方向發展的。
對于你提問的“在Edge和火狐上,redraw事件和setTimeout執行順序問題”,也同樣取決于瀏覽器的內部實現。
我在我電腦的Edge和火狐瀏覽器上進行測試,當時間間隔較小時(如 0~10ms),redraw和setTimeout的執行順序是不固定的。
因此,這需要你經過足夠多的測試,得到一個相對安全的時間值,以確保執行順序的正確性。
參考資料:JavaScript 運行機制詳解:再談Event Loop
JavaScript單線程和瀏覽器事件循環簡述
Javascript是單線程的深入分析
Concurrency model and Event Loop
也談setTimeout
單線程的Javascript
若這篇文章讓您獲益,歡迎您在 Github 給個 Star。
本文鏈接:http://www.codeceo.com/articl...
本文作者:碼農網 – 劉健超
[ 原創作品,轉載必須在正文中標注并保留原文鏈接和作者等信息。]
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/78446.html
摘要:標簽單線程首發地址碼農網細說單線程的一些事最近被同學問道單線程的一些事,我竟回答不上。若以多線程的方式操作這些,則可能出現操作的沖突。另外,因為是單線程的,在某一時刻內只能執行特定的一個任務,并且會阻塞其它任務執行。 標簽: JavaScript 單線程 首發地址:碼農網《細說JavaScript單線程的一些事》 最近被同學問道 JavaScript 單線程的一些事,我竟回答不上。好...
摘要:單線程的好處簡單,處理時不會出現并發競爭問題異步的必要性讓用戶體驗更流暢如何實現異步見參考,,調用棧函數執行上下文。單線程只能有一個并且每次只能執行一個任務。 參考: JavaScript 運行機制詳解:再談Event Loop 深入理解JavaScript的執行過程--單線程的JS 細說JavaScript單線程的一些事 The JavaScript Event Loop: Exp...
摘要:需要注意的是,及更早的瀏覽器不支持第一種語法中向延遲函數傳遞額外參數的功能。如果在不改變遞歸模式的前提下修善這段代碼解決方案加入定時器題目四考察和系列解釋立即的對象,是在本輪事件循環的結束時,而不是在下一輪事件循環的開始時。 前言:setTimeout是JavaScript中常見的一個window對象方法,本文將介紹關于它的一些基礎知識和易出錯的地方。 1、基礎知識 作用:setTim...
摘要:當間隔時間設置較小時,將會導致回調函數堆積。處理可能阻塞的代碼最簡單且最可控的方式就是在回調函數內部使用函數。但是很明顯,由于指定最大值的限制,還會有定時器沒有被清除掉。另外,盡量避免使用函數,從而避免可能導致的回調函數堆積現象。 由于 Javascript 是異步的,因此我們可以通過 setTimeout 和 setInterval 函數來指定特定時間執行代碼。 function ...
摘要:概述本篇主要介紹的運行機制單線程事件循環結論先在中利用運行至完成和非阻塞完成單線程下異步任務的處理就是先處理主模塊主線程上的同步任務再處理異步任務異步任務使用事件循環機制完成調度涉及的內容有單線程事件循環同步執行異步執行定時器的事件循環開始 1.概述 本篇主要介紹JavaScript的運行機制:單線程事件循環(Event Loop). 結論先: 在JavaScript中, 利用運行至...
閱讀 982·2023-04-26 01:47
閱讀 1672·2021-11-18 13:19
閱讀 2042·2019-08-30 15:44
閱讀 645·2019-08-30 15:44
閱讀 2291·2019-08-30 15:44
閱讀 1232·2019-08-30 14:06
閱讀 1420·2019-08-30 12:59
閱讀 1900·2019-08-29 12:49