1. 引言
多執行緒是程式設計中常用的方法,例如,在桌面程式中,主執行緒一般是UI執行緒,負責UI繪製與使用者互動,而運算處理往往是交給背後的工作執行緒,這樣可以有效避免互動時的卡頓感
瀏覽器是多程式的,每開啟一個網頁,都會開啟一個渲染程式,渲染程式包含:
- GUI渲染執行緒 (有且只有一個)
- JS引擎執行緒 (有且只有一個)
- 事件觸發執行緒
- 定時器觸發執行緒
- 非同步http請求執行緒
其中,JS引擎執行緒就是解析JS程式碼的執行緒,由於JS引擎執行緒有且只有一個,所以JS程式碼執行是單執行緒的(筆者注:非同步函式是使用任務佇列實現的,除非呼叫了其他執行緒的函式,如定時器等,不然非同步函式還是單執行緒執行的)
GUI渲染執行緒與JS引擎執行緒是互斥的,且JS引擎執行緒會先執行,如果JS程式碼卡住會導致GUI繪製卡住
有關瀏覽器架構與原理,可以參考:
Web Workers就是建立JS程式碼執行的執行緒,使得JS程式碼執行能以多執行緒的方式執行,避免JS引擎執行緒卡住
有關Web Workers的解釋可以參考:
本文描述瀏覽器中的Web Workers並基於Cesium原始碼進行舉例
2. Web Workers
通常而言,Web Workers包含:
- 專用執行緒(Dedicated Workers)
- 共享執行緒(Shared Workers)
- 後臺執行緒(Service Workers)等
這裡主要是記述通常使用的專用執行緒(Dedicated Workers)
Web Worker的大致的使用方法如下:
(在主執行緒裡)建立一個Web Worker:
const worker = new Worker('WebWorkerTest.js')
WebWorkerTest.js
是Web Worker執行的JS程式碼檔案- 載入Web Worker執行的JS程式碼檔案需要使用HTTP或HTTPS協議,即,需要搭建網路伺服器
(在主執行緒裡)向建立的Web Worker傳送資料:
worker.postMessage([2, 3])
(在子執行緒Web Worker,即WebWorkerTest.js
中)接收主執行緒的資料、處理併傳送給主執行緒:
onmessage = function(e) {
console.log('Message received from main script')
const workerResult = e.data[0] * e.data[1]
console.log('Posting message back to main script')
postMessage(workerResult)
}
(在主執行緒裡)接收Web Worker傳送的資料:
worker.onmessage = (e) => {
console.log("Result:", e.data)
}
綜上,此處建立了兩個檔案:WebWorkerTest.js和WebWorkerTest.html
WebWorkerTest.html程式碼如下:
<body>
<script>
const worker = new Worker('WebWorkerTest.js')
worker.postMessage([2, 3])
worker.onmessage = (e) => {
console.log("Result:", e.data)
}
</script>
</body>
WebWorkerTest.js程式碼如下:
onmessage = function(e) {
console.log('Message received from main script')
const workerResult = e.data[0] * e.data[1]
console.log('Posting message back to main script')
postMessage(workerResult)
}
執行結果如下(使用VS Code和Live Server外掛):
更詳細的Web Worker的使用方法,可以參考以下文件:
3. Cesium中的Web Workers
Cesium原始碼中,對Web Workers進行了封裝,封裝為TaskProcessor
原始碼中給出的TaskProcessor使用示例為:
const taskProcessor = new Cesium.TaskProcessor('myWorkerPath');
const promise = taskProcessor.scheduleTask({
someParameter : true,
another : 'hello'
});
if (!Cesium.defined(promise)) {
// too many active tasks - try again later
} else {
promise.then(function(result) {
// use the result of the task
});
}
檢視原始碼,可以知道taskProcessor.scheduleTask()
函式為:
TaskProcessor.prototype.scheduleTask = function (parameters, transferableObjects) {
// ...
this._worker = createWorker(this);
return Promise.resolve(canTransferArrayBuffer()).then(function (
canTransferArrayBuffer
) {
processor._worker.postMessage(
{
id: id,
parameters: parameters,
canTransferArrayBuffer: canTransferArrayBuffer,
},
transferableObjects
);
return deferred.promise;
});
};
createWorker()
函式為
function createWorker(processor) {
const worker = new Worker(getBootstrapperUrl());
worker.postMessage = defaultValue(
worker.webkitPostMessage,
worker.postMessage
);
// ...
return worker;
}
不難看出,Cesium中將Web Workers封裝成了Promise
,既有操作Promise
的優雅,又有呼叫Web Workers
帶來的多執行緒優勢
例如,在Scene\Primitive.js
中,使用TaskProcessor建立Geometry
:
先是使用createGeometry.js
的檔名建立TaskProcessor:
if (!defined(createGeometryTaskProcessors)) {
createGeometryTaskProcessors = new Array(numberOfCreationWorkers);
for (i = 0; i < numberOfCreationWorkers; i++) {
createGeometryTaskProcessors[i] = new TaskProcessor("createGeometry");
}
}
然後建立promise陣列:
promises.push(
createGeometryTaskProcessors[i].scheduleTask(
{
subTasks: subTasks[i],
},
subTaskTransferableObjects
)
);
最後使用Promise.all
方法執行所有任務並等待結果返回:
Promise.all(promises)
.then(function (results) {
primitive._createGeometryResults = results;
primitive._state = PrimitiveState.CREATED;
})
.catch(function (error) {
setReady(primitive, frameState, PrimitiveState.FAILED, error);
});
4. 參考資料
[1] Web Workers API - Web API 介面參考 | MDN (mozilla.org)
[2] Web Worker 使用教程 - 阮一峰的網路日誌 (ruanyifeng.com)
[3] Cesium原理篇:4Web Workers剖析 - fu*k - 部落格園 (cnblogs.com)
[4] 瀏覽器的程式與執行緒--深入同步、非同步問題 - 知乎 (zhihu.com)