[Note] Event loop, micro-task, macro-task, async JavaScript 筆記
專案程式碼放置於 pjchender/worker-promise-example @ Github
Task(Macrotask)、Microtask、Nanotask
Scheduling Tasks @ HTTP 203
- Priority(執行的優先順序):Sync > Nanotask > Microtask > Task / Macrotask
- Node.js 的應用程式會在單一執行緒中(single thread)運行,而 event loop 也同樣在此 thread 中。雖然如此,但 Node.js 中的許多函式庫並不是單一執行緒在運作的。
- 每一個 thread 都有自己的 event loop,就像是每個 web worker 有自己的 thread 一樣,因此它們可以獨立運行。在 Event Queue 中所有的 Tasks 又可以分成 macrotask 或 microtask。
- macrotask 或簡稱 task 與瀏覽器或電腦底層的運作較有關係,例如
setTimeout
,setInterval
, I/O 等等,系統執行完後,會把寫在裡面的 callback 丟回來 main thread 執行 - microtask 是開發者需要「以非同步的方式來執行同步」的指令時,例如
Promise
,process.nextTick
,queueMicrotask
,MutationObserver
等等,一樣是在 main thread 中執行,主要是改變程式碼執行的時間點- 對於 Promise 來說,
.then()
後面的程式會進入 microtask - 對於 Async Function 來說,該 function 中,第一個
await
後的內容都會進入 microtask - 不論是使用 Promise 或 Async Function,進入 microtask 的程式最終還是會在主執行緒被完成,因此還是有機會卡住主執行緒
- 透過 Worker 可以有效避免主執行去阻塞
- 對於 Promise 來說,
- nanotask 並非正式的名稱,主要是用來表示它的 priority 比 Microtask 還要高,且目前它只存在 Node.js 的環境
備註
- 當
setTimeout
放在其他 task 內時,因為瀏覽器和 Node 實作上的差異,setTimeout 有時無法很精確的反映 Task,相較之下,用MessageChannel
會是比較穩定用來表徵 Task 的方法。 setImmediate
是一個 legacy 的 API(存在 IE),目前仍有在 Node.js 中實作,但它的執行權重最低,比 task 還要後面。
執行緒池(thread pool)
Is Node.js Really Single-Threaded? @ Better Programmer
預設的情況下,Node.JS 會使用 4 個執行緒,若是四核心的電腦,則每個執行緒可以被分配到一顆 CPU 去運作,因此當我們執行下程序時:
// 程式碼來源:https://medium.com/better-programming/is-node-js-really-single-threaded-7ea59bcc8d64
const crypto = require('crypto');
const start = Date.now();
function logHashTime() {
crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => {
console.log('Hash: ', Date.now() - start);
});
}
logHashTime();
logHashTime();
logHashTime();
logHashTime();
會發現可以在幾乎同一時間得到這四個回傳值。但若我們多呼叫一次 logHashTime()
,因為超出了預設的 thread pool 數目,因此第五次的函式會在前四個被執行完後才被接 著執行:
logHashTime();
logHashTime();
logHashTime();
logHashTime();
// 由於預設的 thread pool 只有 4 個 thread,因此最後一個呼叫會被延後等到前四個執行完後才被執行
logHashTime();
若想要增加 thread pool 中 tread 的數目,只需要使用:
process.env.UV_THREADPOOL_SIZE = 5;
也就是:
const crypto = require('crypto');
const start = Date.now();
// 將 thread pool 的 size 設為 5
process.env.UV_THREADPOOL_SIZE = 5;
function logHashTime() {
crypto.pbkdf2('a', 'b', 100000, 512, 'sha512', () => {
console.log('Hash: ', Date.now() - start);
});
}
logHashTime();
logHashTime();
logHashTime();
logHashTime();
logHashTime(); // 不需要等到前 4 個執行完後才被執行
如此,最後一個 logHashTime()
就不需要等到前 4 個被執行完後才能被執行。
現在當我們有 5 個 thread 但只有 4 個 CPU 時,OS thread scheduler 會平衡分派資源給每個 thread,並透過上下文切換(context-switching)來達成。