资讯专栏INFORMATION COLUMN

使用 Web Worker 实现简单的非阻塞异步

junfeng777 / 1188人阅读

摘要:只会把一个函数延后执行,但还是在主线程中执行,执行函数的时候会阻塞线程。规范并没有定义多线程,至今也没有原生的多线程实现。然而在中却定义了用于实现浏览器中的多线程。使用也非常简单,只需要预先在中注册事件,在主线程中给处理就好了。

之前的文章提到了 JavaScript 中的异步编程,然而无论早就存在的 setTimeout 还是 ES6 中的 Promise,它们都是 阻塞 异步,执行函数的时候,会阻塞线程。setTimeout 只会把一个函数延后执行,但还是在主线程中执行,执行函数的时候会阻塞线程。换句话说,setTimeout 只实现了过程间并发(concurrent)而未实现并行(parallel)。

ES 规范并没有定义多线程,Node.js 至今也没有原生的多线程实现。然而在 HTML5 中却定义了 Web Worker 用于实现浏览器中的多线程。

Web Worker

引用 MDN 原文:

Web Workers 使得一个Web应用程序可以在与主执行线程分离的后台线程中运行一个脚本操作。这样做的好处是可以在一个多带带的线程中执行费时的处理任务,从而允许主(通常是UI)线程运行而不被阻塞/放慢。

与朴素(原始)的多线程编程方式不同,Web Worker 通常不允许线程间共享数据,所以没有线程同步、数据竞争等问题,更没有没有锁(Mutex)和条件变量(Condition variable)等概念(注 1)。它们使用 postMessage 相互通信,可以认为是 JS 中的参与者模式实现。各个 Worker 间数据独立,不共享内存:postMessage 始终通过结构化克隆的方式深拷贝传值。

使用 Web Worker 也非常简单,只需要预先在 Worker 中注册 message 事件,在主线程中 postMessage 给 Worker 处理就好了。处理完后可以再通过 postMessage 传结果给主线程。

需要注意的是,Web Worker 中不可以操作 DOM,一切与 DOM 操作相关的函数、类都不能使用(创建一个 DOM 元素发回给主线程 appendChild 也不行),所以可以使用的方法非常有限,只适用于处理数据(注 2)。

使用 Web Worker 实现非阻塞的 Promise

前面提到 Promise 是阻塞异步,那是否可以把要处理的数据转发给某个 Worker 处理并返回一个 Promise,在处理完后将其 resolve 掉呢?

答案当然是可以的,而且实现并不复杂。

创建 Web Worker

首先当然是 new 一个 Worker 出来。需要注意的是 Worker 的构造函数 接受的是一个 JavaScript 脚本的 URL,可否接受 data-uri 看浏览器,实测 Chrome、Firefox 可以,Safari、Edge 不行(会抛 SECURITY_ERR 异常)。

简单起见,这里还是采取 data-uri 的形式。考虑可移植性的话可以先指定一个静态文件,然后使用 postMessage 把函数体传过去。

this._worker = new Worker("data:text/javascript," + encodeURIComponent(`"use strict";
const __fn = ${fn};
onmessage = e => postMessage(__fn(...e.data));`));

Worker 中做了两件事:

定义一个函数变量 __fn,其值 fn 是需要执行的函数。如果 fn 本身是一个函数对象,这里将其转换为字符串,相当于把函数的源代码拼到了字符串里。

绑定 message 事件。将传入的值作为参数列表调用 __fn,然后将 __fn 的返回值通过 postMessage 传给主函数。

当接受请求时,派发事件给创建的 Worker
function dispatch(...args) {
  return new Promise((resolve, reject) => {
    this._queue.push({ resolve, reject });
    this._worker.postMessage(args);
  });
}

返回一个 Promise。注意这里不能只是简单的 postMessage。因为如果使用者多次调用 dispatch 函数一次创建了多个 Promise,之后很难确定是哪个 Promise 完成了。这里通过一个队列记忆创建的 Promise 顺序,然后依次 resolve(单个 Worker 处理 message 事件还是顺序执行的)。当然你也可以多传一个标记值给 Worker 用于标记被 resolve 的 Promise。

JavaScript 里的队列就是数组:

this._queue = [];
接收 Worker 处理完返回的值
this._worker.onmessage = e => this._queue.shift().resolve(e.data);
this._worker.onerror = e => this._queue.shift().reject(e.error);

onmessage 表示正常返回;onerror 表示出现了异常。对应的 Promise 的 resolve 和 reject 直接从队列里取出来。

完整代码
class Dispatcher {
  constructor(fn) {
    this._queue = [];
    this._worker = new Worker("data:text/javascript," + encodeURIComponent(`"use strict";
const __fn = ${fn};
onmessage = e => postMessage(__fn(...e.data));`));
    this._worker.onmessage = e => this._queue.shift().resolve(e.data);
    this._worker.onerror = e => this._queue.shift().reject(e.error);
  }

  dispatch(...args) {
    return new Promise((resolve, reject) => {
      this._queue.push({ resolve, reject });
      this._worker.postMessage(args);
    });
  }
}

这就是完整代码了,总共不到 20 行。使用的话也很简单:

const dispatcher = new Dispatcher(arr => { // 创建对象,把入口函数传入
  for (let i=0; i<1000; ++i) arr.sort(); // 耗费些时间
  return arr;  // 返回处理后的结果
});

const arr = Array.from({ length: 8192 }, () => Math.random() * 10000); // 需要处理的数据
dispatcher.dispatch(arr)  // 派发给 Worker
  .then(res => console.log(res));  // 处理完毕后输出

在浏览器中测试,会生成这样一段代码:

排序大数组 1000 次的同时 UI 响应仍然不受影响。

这里还有一个线程池的版本,可以创建多个 Worker 同时并行执行多个任务:https://github.com/CarterLi/T...

因为要区分究竟是哪个 Worker 完成运行,处理 Worker 返回值的逻辑复杂了一些,有什么建议欢迎提出。

注 1:ES2017 中加入 SharedArrayBuffer 后已经可以在主线程和各 Web Worker 间共享数据,使用 Atomics.wait()Atomics.wake() 还可以实现传统意义上的锁和条件变量。但由于其出现较晚且并非使用 Web Worker 的主流方式,这里不展开讨论。

注 2:还有一个可能是在 Worker 中画图,见 OffscreenCanvas。一旦实现,对游戏编程是个不小的帮助。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90395.html

相关文章

  • 深入 Nginx 之架构篇

    摘要:请求的多阶段异步处理多阶段异步处理请求与事件驱动架构是密切相关的,也就是说,请求的多阶段异步处理只能基于事件驱动架构实现。 前言 最近在读 Nginx 相关的书籍,做一下读书笔记。 Nginx 作为业界知名的高性能服务器,被广泛的应用。它的高性能正是由于其优秀的架构设计,其架构主要包括这几点:模块化设计、事件驱动架构、请求的多阶段异步处理、管理进程与多工作进程设计、内存池的设计,以下内...

    linkin 评论0 收藏0
  • 深入 Nginx 之架构篇

    摘要:请求的多阶段异步处理多阶段异步处理请求与事件驱动架构是密切相关的,也就是说,请求的多阶段异步处理只能基于事件驱动架构实现。 前言 最近在读 Nginx 相关的书籍,做一下读书笔记。 Nginx 作为业界知名的高性能服务器,被广泛的应用。它的高性能正是由于其优秀的架构设计,其架构主要包括这几点:模块化设计、事件驱动架构、请求的多阶段异步处理、管理进程与多工作进程设计、内存池的设计,以下内...

    zhangrxiang 评论0 收藏0
  • 深入理解Node.js 进程与线程(8000长文彻底搞懂)

    摘要:在单核系统之上我们采用单进程单线程的模式来开发。由进程来管理所有的子进程,主进程不负责具体的任务处理,主要工作是负责调度和管理。模块与模块总结无论是模块还是模块,为了解决实例单线程运行,无法利用多核的问题而出现的。 前言 进程与线程是一个程序员的必知概念,面试经常被问及,但是一些文章内容只是讲讲理论知识,可能一些小伙伴并没有真的理解,在实际开发中应用也比较少。本篇文章除了介绍概念,通过...

    Harpsichord1207 评论0 收藏0
  • JavaScript 工作原理之七-Web Workers 分类及 5 个使用场景

    摘要:最后,我们将会介绍个的使用场景。异步编程的局限性前面我们了解到异步编程及其使用时机。请求是一个很好的异步编程的使用场景。整个是基于单线程环境的而部分可以突破这方面的限制。最佳使用场景迄今为止,我们列举了的长处及其限制。 Web Workers 分类及 5 个使用场景 原文请查阅这里,略有删减,本文采用知识共享署名 4.0 国际许可协议共享,BY Troland。 这是 JavaScri...

    cartoon 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<