本文概述
我们的天体物理学模拟器由希望, 炒作和获得新的计算能力的强大火箭燃料混合物提供动力。
我们可以通过网络工作者来访问这种计算能力。如果你已经熟悉Web工作者, 则可能需要查看代码并跳到WebAssembly, 这将在下一篇文章中进行讨论。
JavaScript成为安装最多, 学习最多和可访问性最强的编程语言, 因为它为静态Web带来了一些非常有用的功能:
- 单线程事件循环
- 异步代码
- 垃圾收集
- 无需严格输入的数据
单线程意味着我们不必担心多线程编程的复杂性和陷阱。
异步意味着我们可以将函数作为参数传递给以后执行-作为事件循环中的事件。
这些功能以及Google对Chrome V8 JavaScript引擎性能的巨额投资, 再加上良好的开发人员工具, 使JavaScript和Node.js成为微服务架构的理想选择。
对于必须在计算机的多核之间安全隔离并运行所有间谍软件感染的浏览器选项卡运行时的浏览器制造商来说, 单线程执行也非常有用。
问:一个浏览器选项卡如何访问你计算机的所有CPU内核?
答:网络工作者!
网络工作者和线程
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()
}
网络工作者的贡献是为WebAssembly托管一个单独的线程。作为一种低级语言, WebAssembly仅理解整数和浮点数。我们无法传递JavaScript字符串或对象-只能传递指向”线性内存”的指针。因此, 为方便起见, 我们将”实体”打包为一系列浮点数:arrBodies。
我们将在关于WebAssembly和AssemblyScript的文章中再次谈到这一点。
将数据移入/移出Web Worker
在这里, 我们正在创建一个Web worker, 以在单独的线程中运行calculateForces()。这是在以下情况发生的, 即我们将物体的(x, y, z, 质量)编组为一个浮点数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的运行main.js的index.html会创建一个新的nBodySimulator(), 并在其构造函数中找到setupWebWorker()。
// nBodySimulator.js
/**
* Our n-body system simulator
*/
export class nBodySimulator {
constructor() {
this.setupWebWorker()
...
我们新的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()函数。
请记住, 网络工作者无法访问网络, 因此主UI线程必须将已编译的WebAssembly代码作为消息resolve(“动作打包”)传递给网络工作者。在下一篇文章中, 我们将深入探讨这一点。
// 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()模式收听网络工作者的消息。
// 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()创建并返回一个promise, 将resolve()和拒绝()保存为self.forcesReject()和self.forcesResolve()。
这样, worker.onmessage()可以解析在calculateForces()中创建的promise。
如果你还记得我们的仿真循环的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()并重新应用以前的作用力。
此步进功能每33ms触发一次。如果网络工作者尚未准备就绪, 它将应用并绘制以前的力。如果某个特定步骤的calculateForces()在下一步骤的开始之后仍然有效, 则下一步骤将从上一步的位置开始施加力。这些先前的力要么足够相似以至于看起来”正确”, 要么发生得如此之快以至于用户无法理解。即使不建议实际的人类太空旅行, 这种权衡也会提高感知性能。
这可以改善吗?是!对于setInterval, 我们的step函数的替代方法是requestAnimationFrame()。
就我的目的而言, 这足以探索Canvas, WebVR和WebAssembly。如果你认为可以添加或替换某些内容, 请随时发表评论或取得联系。
如果你正在寻找现代, 完整的物理引擎设计, 请查看开源的Matter.js。
WebAssembly呢?
WebAssembly是可移植的二进制文件, 可在浏览器和系统之间运行。 WebAssembly可以用多种语言(C / C ++ / Rust等)进行编译。出于我自己的目的, 我想尝试AssemblyScript-一种基于TypeScript的语言, 它是一种基于JavaScript的语言, 因为它一直都是乌龟。
AssemblyScript将TypeScript代码编译为可移植的”目标代码”二进制文件, 以”及时”编译为称为Wasm的新型高性能运行时。将TypeScript编译为.wasm二进制文件时, 可以创建描述二进制文件的.wat人类可读的”网络程序集文本”格式。
setupWebWorker()的最后一部分开始我们在WebAssembly上的下一篇文章, 并显示如何克服Web Worker对网络访问的限制。我们在主UI线程中获取wasm文件, 然后”及时”将其编译为本地wasm模块。我们将该模块postMessage()作为消息发送给Web Worker:
// 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, 质量)。尽管复制内存价格便宜, 但这并不是成熟的高性能设计。
幸运的是, 许多非常聪明的人正在努力创建这些尖端浏览器技术的规范和实现。
JavaScript具有SharedArrayBuffer来创建一个共享内存对象, 该对象将消除调用中(2)->(3)的postMessage()s副本和结果(3)->(2)的arrForces的onmessage()s副本。
WebAssembly还具有线性内存设计, 可以为(3)->(4)中的nBodyForces()调用托管共享内存。网络工作者还可以为结果数组传递共享内存。
下次加入我们, 踏上JavaScript内存管理的激动人心的旅程。