Cesium之Web Workers

當時明月在曾照彩雲歸發表於2023-04-25

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.jsWebWorkerTest.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外掛):

image-20230425142342609

更詳細的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)

[5] Web Worker 之全面講解 - 知乎 (zhihu.com)

[6] 使用 Service Worker - Web API 介面參考 | MDN (mozilla.org)

相關文章