你懂 JavaScript 嗎?#22 非同步:現在和以後
29 Oct 2018本文主要會談到
- 事件迴圈
- 共時
事件迴圈(Event Loop)
開發者會利用函式(function)的方式將程式碼切成一個個片段,而這些函式執行的時機是由一種排程機制所控管的-由「事件迴圈」來控制。事件迴圈是一種類似佇列(queue)的機制,用來管理何時執行哪一個函式。當要執行這個函式時,就將它放到事件迴圈中等候執行。例如:發送 ajax 後要執行某個 callback,於是告訴宿主環境在收到回應時將這個 callback 放到 queue 中準備執行。另外,每一次 loop 稱為一個 tick,每個 tick 會取出 queue 中的 job,也就是要執行的函式來執行。
…
…
一種很多人在排隊的 feel ~
…
…
如下所示,這是一個事件迴圈選取工作執行的虛擬碼。eventLoop 是一個佇列,存放準備要執行的工作,每次會從中取出一個工作來執行,而這個迴圈是常駐的,永遠沒有結束的時候。
var eventLoop = [];
var event;
while (true) {
// 開始本次 tick ...
if (eventLoop.length > 0) {
// 取得工作
event = eventLoop.shift();
// 執行工作
try {
event();
} catch (err) {
reportError(err);
}
}
}
圖解。
備註
- 宿主環境(hosting environment):JavaScript 常見的宿主環境是瀏覽器,或是 Node.js 所在的伺服器環境。
共時(Concurrency)
共時是指兩個以上的行程(process)在同一時間內同時執行。但對於事件迴圈佇列來說,工作是循序執行的,不會同一瞬間執行多個工作,共時的行程可以彼此沒有互動或有互動。
若有兩個行程 P1 與 P2 在一時間區段內交互執行,也就是有互動的狀況,P1 發出請求 Req i 後,P2 會依照回應 Res i 做出相對應的處理。也就是說,當 P1 與 P2 開始執行後,會有以下的請求和回應:
Req 1, Res 1, Req 2, Req 3, Res 3, Res 2
由此可知,請求和回應的順序是無法預期的。
關於共時的資源利用,有以下兩個議題:「值的共用」和「合作式共時」。
值的共用(Value Sharing)
使用一個全域變數(global variable)儲存結果,例如:「result」,P1 與 P2 一起存取 result。這裡要用一些小技巧來避免競爭狀態(race condition),例如:使用閘門(gate)或閂鎖(latch)。
…
…
共用吃一顆蘋果?
…
…
備註
- 閘門(gate):條件皆滿足才能存取。
- 閂鎖(latch):第一名使用者存取完畢後,變數即不可重新設值。簡單的範例就是常用 flag 來檢查是否已做過某件事,flag 初始值為 false,做了指定的工作後設為 true。每次執行先檢查 flag 的值,若為 false 則做這個工作;反之,若為 true 則跳過這個工作。
var isDone = false; // flag
if (!isDone) {
isDone = true;
// 執行工作
}
合作式共時(Cooperative Concurrency)
獨佔
目前執行的工作耗費太多時間,形成獨佔資源的狀態,導致其他工作必須長時間等待,解決方法就是將這個工作切割成片段,逐步完成。
分散
將長時間佔用資源的工作切割成多個子工作,逐步完成,好處是讓其他工作能排入事件迴圈佇列。對使用者來說,有種工作很快被完成的錯覺。
圖片來源:Vida Diamante: Como Administrar Bien Tu Tiempo
…
…
在下例中,發送一個 ajax request,再由 response 這個 callback 處理回傳結果。由於回傳後的結果 data 的資料量可能非常大,處理起來需要花費非常久的時間,因此先切割成兩份,前一千份資料放在 chunk 中,其餘資料仍放在 data 中,然後先對 chunck 做處理,待之後再使用 setTimeout(function() { ... }, 0 )
這個 hack 的方式將剩餘的資料和其處理工作加入事件迴圈佇列中。
var res = [];
function response(data) {
var chunk = data.splice(0, 1000);
res = res.concat(
chunk.map(function (val) {
return val * 2;
}),
);
if (data.length > 0) {
setTimeout(function () {
response(data);
}, 0);
}
}
ajax('http://some.url', response);
工作佇列(Job Queue)
這邊要特別說明,使用 setTimeout(function() { ... }, 0 )
加入事件迴圈佇列並無法確定執行時間,只是將這個工作放到目前隊伍(工作佇列,job queue)的尾端,待下一個 tick 優先執行,這是一個代辦清單的概念,是一種插隊的行為。
console.log('A');
setTimeout(function () {
console.log('B');
}, 0);
schedule(function () {
console.log('C');
schedule(function () {
console.log('D');
});
});
假設 schedule 是一個假想的事件迴圈佇列排程的 API,使用 setTimeout(function() { ... }, 0 )
加入一個工作 B,B 會放到目前佇列的尾端,等待下一個 tick 優先執行。
得到 A -> C -> D -> B。
回顧
看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…
- 事件迴圈是一種類似佇列的機制,用來管理何時執行哪一個函式。
- 共時是指兩個以上的行程在同一時間內同時執行。關於共時的資源利用可分為兩種「值的共用」與「合作式共時」。
References
同步發表於2019 鐵人賽。