使用 Web Worker + SharedArrayBuffer 并行实现视频实时滤镜效果

之前分别用 JSGolang WebAssemblyRust WebAssembly 实现过视频实时滤镜效果,证明了 WebAssembly 的性能确实要更好一些。但其实当处理图像这种较大的数据时,另一个比较常用的做法是分块并行处理,这就要用到 Web Worker 了。

思路也很简单,在主进程中我们把图像分成好几个块,然后传给各个 Web Worker 进行处理,然后接受处理后的结果拼接起来。不过在进程间传递大量数据显然是一个比较低效的方式,所以这里我们使用 SharedArrayBuffer,让主进程跟各 Web Worker 之间共享一块内存,这样就只需要传递 SharedArrayBuffer 的引用就可以了:

js 复制代码
// main.js
const worker = new Worker('...')
const sharedArrayBuffer = new SharedArrayBuffer(imageData.buffer.byteLength)
new Uint8ClampedArray(sharedArrayBuffer).set(imageData)

worker.postMessage({start, end, width, sharedArrayBuffer})

// webworker.js
onmessage = async (e: MessageEvent) => {
  const {
    data: {sharedArrayBuffer, start, end, width},
  } = e
  const uint8ClampedArray = new Uint8ClampedArray(sharedArrayBuffer)

  // update sharedArrayBuffer using uint8ClampedArray
  for (let i = start; i < end; i++) {
    for (let j = 0; j < width; j++) {
      uint8ClampedArray[i * width + j] = 100 // Red
      uint8ClampedArray[i * width + j + 1] = 100 // Green
      uint8ClampedArray[i * width + j + 2] = 100 // Blue
    }
  }
}

上述代码中的 startend 分别表示当前 Web Worker 需要处理的起始和结束行号,width 表示 Canvas 宽度。用图来表示就像这样:

把 Canvas 宽高设大一点后,对比之前的 JS 版本,效果还是很明显的:

到这,你肯定会想到,如果在 Web Worker 里面再用上 WebAssembly 那岂不是效果更炸裂?

我们来试试,只需要在 Web Worker 里面再把数据共享给 WASM 处理,然后再使用修改后的 memory.buffer 来更新 SharedArrayBuffer 对应的块就可以了:

js 复制代码
onmessage = async (e: MessageEvent) => {
  const {
    data: {sharedArrayBuffer, start, end, width},
  } = e
  const ptr = return_pointer()
  const uint8ClampedArrayForMemBuf = new Uint8ClampedArray(memory.buffer)
  const uint8ClampedArrayForSharedBuf = new Uint8ClampedArray(sharedArrayBuffer)
  // Sync the block data to WASM
  uint8ClampedArrayForMemBuf.set(
    uint8ClampedArrayForSharedBuf.slice(start * width * 4, end * width * 4)
  )
  // Image convolution
  filter_shared_mem(
    ptr,
    width,
    end - start,
    new Float32Array([].concat(...kernel))
  )
  // Update the block in SharedArrayBuffer using modified memory.buffer
  uint8ClampedArrayForSharedBuf.set(
    new Uint8ClampedArray(memory.buffer).slice(ptr, (end - start) * width * 4),
    start * width * 4
  )
}

这里需要注意的地方是从 SharedArrayBuffer 中同步数据到 memory.buffer 以及更新 SharedArrayBuffer 中的数据时,都只需要关注前 Web Worker 所属的块就可以了。

看一下效果,确实又提升了:

而且,通过分块的方式,还解决了另外一个内存相关的问题。为什么这么说呢,因为增大了 canvas 的宽高,相当于增加了需要处理的图像数据大小,此时如果切到图中的 Rust WebAssembly (Shared Memory) 会报 RangeError: offset is out of bounds 错误,原因在于图像大小已经超过了 WebAssembly 中初始的 Memory 的大小。虽然 MDN 上介绍了可以使用 memory.grow() 方法来扩大内存,但是还是会有别的错误,暂时未能解决。

欢迎关注公众号"前端游"

相关推荐
四岁半儿28 分钟前
常用css
前端·css
你的人类朋友1 小时前
说说git的变基
前端·git·后端
姑苏洛言1 小时前
网页作品惊艳亮相!这个浪浪山小妖怪网站太治愈了!
前端
字节逆旅1 小时前
nvm 安装pnpm的异常解决
前端·npm
Jerry2 小时前
Compose 从 View 系统迁移
前端
GIS之路2 小时前
2025年 两院院士 增选有效候选人名单公布
前端
四岁半儿2 小时前
vue,H5车牌弹框定制键盘包括新能源车牌
前端·vue.js
烛阴2 小时前
告别繁琐的类型注解:TypeScript 类型推断完全指南
前端·javascript·typescript
gnip2 小时前
工程项目中.env 文件原理
前端·javascript
JefferyXZF3 小时前
Next.js Server Actions 详解: 无缝衔接前后端的革命性技术(八)
前端·全栈·next.js