WebVR 第 2 部分:Web Workers 和瀏覽器邊緣計算
已發表: 2022-03-11我們的天體物理學模擬器由一種強大的火箭燃料混合物提供動力,該燃料混合了希望、炒作和獲得新計算能力的途徑。
我們可以通過web workers訪問這種計算能力。 如果您已經熟悉 Web Workers,您可能想深入了解代碼並跳到 WebAssembly,這將在下一篇文章中討論。
JavaScript 成為安裝、學習和訪問最多的編程語言,因為它為靜態 Web 帶來了一些非常有用的功能:
- 單線程事件循環
- 異步代碼
- 垃圾收集
- 沒有嚴格類型的數據
單線程意味著我們不必過多擔心多線程編程的複雜性和陷阱。
異步意味著我們可以傳遞函數作為稍後執行的參數——作為事件循環中的事件。
這些特性和谷歌對 Chrome 的 V8 JavaScript 引擎性能的大量投資,以及優秀的開發工具,使 JavaScript 和 Node.js 成為微服務架構的完美選擇。
單線程執行也非常適合瀏覽器製造商,他們必須跨計算機的多個內核安全地隔離和運行所有受間諜軟件感染的瀏覽器選項卡運行時。
問題:一個瀏覽器選項卡如何訪問您計算機的所有 CPU 內核?
答:網絡工作者!
Web Worker 和線程
Web Worker 使用事件循環在線程之間異步傳遞消息,繞過了多線程編程的許多潛在缺陷。
Web worker 也可用於將計算移出主 UI 線程。 這讓主 UI 線程可以處理點擊、動畫和管理 DOM。
讓我們看一下該項目的 GitHub 存儲庫中的一些代碼。
如果您還記得我們的架構圖,我們將整個模擬委託給nBodySimulator
,以便它管理 Web Worker。
如果您還記得介紹文章中的內容, nBodySimulator
有一個step()
函數,每 33ms 的模擬調用一次。 它調用calculateForces()
,然後更新位置並重新繪製。
// Methods from class nBodySimulator /** * The simulation loop */ start() { // This is the simulation loop. step() calls visualize() const step = this.step.bind(this) setInterval(step, this.simulationSpeed) } /** * A step in the simulation loop. */ async step() { // Skip calculation if worker not ready. Runs every 33ms (30fps), expect it to skip. if (this.ready()) { await this.calculateForces() } else { console.log(`Skipping calculation: WorkerReady: ${this.workerReady} WorkerCalculating: ${this.workerCalculating}`) } // Remove any "debris" that has traveled out of bounds - this is for the button this.trimDebris() // Now Update forces. Reuse old forces if we skipped calculateForces() above this.applyForces() // Ta-dah! this.visualize() }
Web Worker 的貢獻是為 WebAssembly 託管一個單獨的線程。 作為一種低級語言,WebAssembly 只理解整數和浮點數。 我們不能傳遞 JavaScript字符串或對象——只是指向“線性內存”的指針。 所以為了方便起見,我們將我們的“body”打包成一個浮點數組: arrBodies
。
我們將在關於 WebAssembly 和 AssemblyScript 的文章中再次討論這一點。
在這裡,我們創建了一個 web worker 來在一個單獨的線程中運行calculateForces()
。 這發生在下面,因為我們將物體 (x, y, z, mass) 編組為浮點數arrBodies
,然後將this.worker.postMessage()
到工人。 我們返回一個承諾,工人將在this.worker.onMessage()
稍後解決。
// src/nBodySimulator.js /** * Use our web worker to calculate the forces to apply on our bodies. */ calculateForces() { this.workerCalculating = true this.arrBodies = [] // Copy data to array into this.arrBodies ... // return promise that worker.onmessage will fulfill const ret = new Promise((resolve, reject) => { this.forcesResolve = resolve this.forcesReject = reject }) // postMessage() to worker to start calculation // Execution continues in workerWasm.js worker.onmessage() this.worker.postMessage({ purpose: 'nBodyForces', arrBodies: this.arrBodies, }) // Return promise for completion // Promise is resolve()d in this.worker.onmessage() below. // Once resolved, execution continues in step() above - await this.calculateForces() return ret }
從頂部開始,瀏覽器 GET 的index.html
運行main.js
,它創建一個new nBodySimulator()
並在其構造函數中找到setupWebWorker()
。
// nBodySimulator.js /** * Our n-body system simulator */ export class nBodySimulator { constructor() { this.setupWebWorker() ...
我們的new nBodySimulator()
位於主 UI 線程中, setupWebWorker()
通過從網絡中獲取workerWasm.js
來創建 web worker。
// nBodySimulator.js // Main UI thread - Class nBodySimulator method setupWebWorker() { // Create a web worker (separate thread) that we'll pass the WebAssembly module to. this.worker = new Worker("workerWasm.js"); // Console errors from workerWasm.js this.worker.onerror = function (evt) { console.log(`Error from web worker: ${evt.message}`); } ...
在new Worker()
中,瀏覽器在單獨的 JavaScript 運行時(和線程)中獲取並運行workerWasm.js
,並開始傳遞消息。
然後, workerWasm.js
進入了 WebAssembly,但它實際上只是一個包含switch()
語句的this.onmessage()
函數。
請記住,Web Worker 無法訪問網絡,因此主 UI 線程必須將編譯後的 WebAssembly 代碼作為消息傳遞給 Web Worker resolve("action packed")
。 我們將在下一篇文章中深入探討。
// workerWasm.js - runs in a new, isolated web worker runtime (and thread) this.onmessage = function (evt) { // message from UI thread var msg = evt.data switch (msg.purpose) { // Message: Load new wasm module case 'wasmModule': // Instantiate the compiled module we were passed. ... // Tell nBodySimulation.js we are ready this.postMessage({ purpose: 'wasmReady' }) return // Message: Given array of floats describing a system of bodies (x, y, z, mass), // calculate the Grav forces to be applied to each body case 'nBodyForces': ... // Do the calculations in this web worker thread synchronously const resultRef = wasm.nBodyForces(dataRef); ... // See nBodySimulation.js' this.worker.onmessage return this.postMessage({ purpose: 'nBodyForces', arrForces }) } }
回到我們的nBodySimulation
類的setupWebWorker()
方法,我們使用相同的onmessage() + switch()
模式來監聽 web worker 的消息。

// Continuing class nBodySimulator's setupWebWorker() in the main UI thread // Listen for messages from workerWasm.js postMessage() const self = this this.worker.onmessage = function (evt) { if (evt && evt.data) { // Messages are dispatched by purpose const msg = evt.data switch (msg.purpose) { // Worker's reply that it has loaded the wasm module we compiled and sent. Let the magic begin! // See postmessage at the bottom of this function. case 'wasmReady': self.workerReady = true break // wasm has computed forces for us // Response to postMessage() in nBodySimulator.calculateForces() above case 'nBodyForces': self.workerCalculating = false // Resolve await this.calculateForces() in step() above if (msg.error) { self.forcesReject(msg.error) } else { self.arrForces = msg.arrForces self.forcesResolve(self.arrForces) } break } } } ...
在這個例子中, calculateForces()
創建並返回一個保存resolve()
和reject()
作為self.forcesReject()
和self.forcesResolve()
的承諾。
這樣, worker.onmessage()
可以解決在calculateForces()
中創建的承諾。
如果你還記得我們模擬循環的step()
函數:
/** * This is the simulation loop. */ async step() { // Skip calculation if worker not ready. Runs every 33ms (30fps), expect it to skip. if (this.ready()) { await this.calculateForces() } else { console.log(`Skipping calculation: WorkerReady: ${this.workerReady} WorkerCalculating: ${this.workerCalculating}`) }
如果 WebAssembly 仍在計算,這讓我們可以跳過calculateForces()
並重新應用之前的力。
此步進函數每 33 毫秒觸發一次。 如果 web worker 沒有準備好,它會應用並繪製先前的力量。 如果特定步驟的calculateForces()
在下一步開始之後起作用,則下一步將從上一步的位置施加力。 這些先前的力量要么相似到看起來“正確”,要么發生得如此之快以至於用戶無法理解。 這種權衡提高了感知性能——即使不建議將其用於實際的人類太空旅行。
這可以改進嗎? 是的! 對於我們的 step 函數, setInterval
的替代方法是requestAnimationFrame()
。
就我而言,這足以探索 Canvas、WebVR 和 WebAssembly。 如果您認為可以添加或更換某些內容,請隨時發表評論或與我們聯繫。
如果您正在尋找現代的、完整的物理引擎設計,請查看開源的 Matter.js。
WebAssembly 怎麼樣?
WebAssembly 是一個可移植的二進製文件,可以跨瀏覽器和系統工作。 WebAssembly 可以從多種語言(C/C++/Rust 等)編譯。 出於我自己的目的,我想嘗試 AssemblyScript——一種基於 TypeScript 的語言,它是一種基於 JavaScript 的語言,因為它一直是烏龜。
AssemblyScript 將 TypeScript 代碼編譯為可移植的“目標代碼”二進製文件,以便“即時”編譯到名為 Wasm 的新高性能運行時中。 將 TypeScript 編譯成.wasm
二進製文件時,可以創建一個.wat
人類可讀的“Web 彙編文本”格式來描述二進製文件。
setupWebWorker()
的最後一部分開始了我們關於 WebAssembly 的下一篇文章,並展示瞭如何克服 Web Worker 對網絡訪問的限制。 我們在主 UI 線程中fetch()
wasm
文件,然後“即時”將其編譯為原生 wasm 模塊。 我們postMessage()
將該模塊作為消息發送給網絡工作者:
// completing setupWebWorker() in the main UI thread … // Fetch and compile the wasm module because web workers cannot fetch() WebAssembly.compileStreaming(fetch("assembly/nBodyForces.wasm")) // Send the compiled wasm module to the worker as a message .then(wasmModule => { self.worker.postMessage({ purpose: 'wasmModule', wasmModule }) }); } }
workerWasm.js
然後實例化該模塊,以便我們可以調用它的函數:
// wasmWorker.js - web worker onmessage function this.onmessage = function (evt) { // message from UI thread var msg = evt.data switch (msg.purpose) { // Message: Load new wasm module case 'wasmModule': // Instantiate the compiled module we were passed. wasm = loader.instantiate(msg.wasmModule, importObj) // Throws // Tell nBodySimulation.js we are ready this.postMessage({ purpose: 'wasmReady' }) return case 'nBodyForces': ... // Do the calculations in this thread synchronously const resultRef = wasm.nBodyForces(dataRef);
這就是我們訪問 WebAssembly 功能的方式。 如果您正在查看未編輯的源代碼,您會注意到...
是一堆內存管理代碼,用於將我們的數據放入dataRef
並將我們的結果從resultRef
中取出。 JavaScript 中的內存管理? 令人興奮!
我們將在下一篇文章中更詳細地研究 WebAssembly 和 AssemblyScript。
執行邊界和共享內存
這裡還有一些東西要談,那就是執行邊界和共享內存。
WebAssembly 的文章很有戰術性,所以這裡是討論運行時的好地方。 JavaScript 和 WebAssembly 是“模擬的”運行時。 正如實現的那樣,每次我們跨越運行時邊界時,我們都會復制我們的身體數據(x,y,z,mass)。 雖然複製內存很便宜,但這並不是成熟的高性能設計。
幸運的是,許多非常聰明的人正在致力於創建這些尖端瀏覽器技術的規範和實現。
JavaScript 有 SharedArrayBuffer 來創建一個共享內存對象,該對象將在調用中消除來自 (2) -> (3) 的postMessage()
的副本,並在結果上消除來自 (3) -> (2) 的arrForces
的onmessage()
的副本.
WebAssembly 還具有線性內存設計,可以為 (3) -> (4) 的nBodyForces()
調用託管共享內存。 web worker 也可以為結果數組傳遞一個共享內存。
下次加入我們,開啟一段激動人心的 JavaScript 內存管理之旅。