关于javascript:JS-多线程并发

6次阅读

共计 3609 个字符,预计需要花费 10 分钟才能阅读完成。

为什么须要并发

咱们常据说 JS 是单线程模型,即所有代码都在主线程中执行的。
如果某些工作计算量较大,将阻塞主线程,UI 界面轻则掉帧、重则卡死。

// 在任意网页控制台执行以下代码,页面将卡住 3s
function execTask() {const t = performance.now()
    // 模仿耗时工作
    while(performance.now() - t < 3000){}}
execTask()

所以在计算量大的场景,JS 须要反对并发能力,防止主线程阻塞,影响用户体验。

并发面临的问题

用一个极简化示例,来阐明并发面临问题:
10 个线程同时执行 1000 个工作,如何 防止某个工作被反复 执行?

办法 1:
工作列表对线程不可见,而是新开一个线程来统一分配工作,并收集其余线程的执行后果。

办法 2:
工作列表对所有线程可见(共享内存),线程 先排队去支付工作编号,而后执行对应编号的工作。

拓展浏览并发问题

JS 中如何实现上述两种办法

JS 采纳 Web Worker API 来实现多线程并发。

分配任务,多 Worker 执行

function workerSetup() {self.onmessage = (evt) => {const t = performance.now()
    // 模仿耗时工作, 随机耗费工夫 0~100ms
    while(performance.now() - t < Math.random() * 100){}

    const {idx, val} = evt.data
    // 实际上只是算一下参数的平方
    self.postMessage({
      idx: idx,
      val: val * val
    })
  }
}
// 创立一个运行 workerSetup 函数的 worker
const createWorker = () => {const blob = new Blob([`(${workerSetup.toString()})()`])
  const url = URL.createObjectURL(blob)
  return new Worker(url)
}
// 模仿 1000 个工作
const tasks = Array(1000).fill(0).map((_, idx) => idx + 1)
const result = []
let rsCount = 0
const onMsg = (evt) => {result[evt.data.idx] = evt.data.val
  rsCount += 1
  // 所有工作实现时打印后果
  if (rsCount === tasks.length) {console.log('task:', tasks)
    console.log('result:', result)
  }
}

// 模仿线程池
const workerPool = Array(10).fill(0).map(createWorker)
workerPool.forEach((worker, idx) => {
  worker.onmessage = onMsg
  worker.id = idx
})

for (const idx in tasks) {
  // 随机分配任务
  const worker = workerPool[Math.floor(Math.random() * workerPool.length)]
  worker.postMessage({idx, val: tasks[idx] })
  console.log(`Worker ${worker.id}, process task ${idx}`)
}

多 Worker 共享工作(内存)

SharedArrayBuffer 是 JS 提供的惟一可在不同线程间共享内存的形式。

为应答幽灵破绽,所有支流浏览器均默认于 2018 年 1 月 5 日禁用 SharedArrayBuffer。
在 2020 年,一种新的、平安的办法曾经标准化,以从新启用 SharedArrayBuffer。
须要设置两个 HTTP 音讯头以跨域隔离你的站点:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

:::tip
在浏览器中执行以下代码,请先确保 SharedArrayBuffer 可用。
可复制代码在 该页面 的控制台执行测试
:::

function workerSetup() {function execTask(val) {const t = performance.now()
    // 模仿耗时工作, 随机耗费工夫 0~100ms
    while (performance.now() - t < Math.random() * 100) {}
    return val * val
  }
  self.onmessage = (evt) => {const { idx, sab} = evt.data
    const uint16Arr = new Uint16Array(sab)
    while(true){
      // 模仿排队支付工作
      // 如果应用 taskNo = uint16Arr[0]获取工作编号,会呈现抢工作的景象(反复执行工作)const taskNo = Atomics.add(uint16Arr, 0, 1) 
      if (taskNo >= uint16Arr.length) break
      
      // 每个工作写不同的地位,所以不须要原子操作
      uint16Arr[taskNo] = execTask(uint16Arr[taskNo])
      console.log(`Worker ${idx}, process task ${taskNo}`)
    }
    self.postMessage(true)
  }
}

const createWorker = () => {const blob = new Blob([`(${workerSetup.toString()})()`])
  const url = URL.createObjectURL(blob)
  return new Worker(url)
}

// 第一位寄存下一个工作编号,前面 1000 寄存对应工作及后果
const sab = new SharedArrayBuffer((1 + 1000) * 2)
const uint16Arr = new Uint16Array(sab)
uint16Arr[0] = 1
for (let i = 1; i < uint16Arr.length; i++) {uint16Arr[i] = i 
}
// 模仿线程池,创立 10 个 worker
const workerPool = Array(10).fill(0).map(createWorker)

let rsCount = 0
const onMsg = () => {
  rsCount += 1
  if (rsCount === workerPool.length) {console.log('result:', uint16Arr, sab)
  }
}
workerPool.forEach((worker, idx) => {
  worker.onmessage = onMsg
  worker.postMessage({idx, sab})
})

Atomics 对象提供了一组静态方法对 SharedArrayBuffer 对象进行原子操作。

两个办法比照

办法 1(分配任务)

解决 1000 个工作,调用了 2000 次(分配任务、反馈后果)postMessage,也就是数据在两个 worker 间传递,经验了 2000 次结构化克隆。
通常来说结构化克隆的速度比拟快,影响不大

Even on the slowest devices, you can postMessage() objects up to 100KiB and stay within your 100ms response budget. If you have JS-driven animations, payloads up to 10KiB are risk-free. This should be sufficient for most apps.
即便在十分慢的设施上,你也能够应用 postMessage() 传递 100KiB 的对象,可保障在 100 毫秒内响应。如果有用 JS 驱动的动画,那么传递 10KiB 的数据是无风险的。这对于大多数应用程序来说应该足够了。

另外,局部原生对象是 Transferable objects,postMessage(arrayBuffer, [arrayBuffer]) 能够 传递这些对象对所有权,无需 clone
目前实现 Transferrable 的对象有:ArrayBuffer, MessagePort, ReadableStream, WritableStream, TransformStream, AudioData, ImageBitmap, VideoFrame, OffscreenCanvas, RTCDataChannel

所以应优先采纳该办法

办法 2(共享内存)

共享内存(SharedArrayBuffer)节俭了线程间通信的耗费,但减少了代码复杂性,只能共享二进制数据,且 ShareArrayBuffer、Atomics 有肯定的兼容性问题。
(目前我还没碰到必须应用 SharedArrayBuffer 的场景,只看到 WASM 软解 HEVC 用到了)

正文完
 0