likes
comments
collection
share

前端多线程在开发中的应用总结

作者站长头像
站长
· 阅读数 56

应该都知道 JavaScript 本身是单线程的,也就是说在一个给定的时间内,JavaScript 只能执行一个任务。然而HTML5 引入了 Web Workers API,使得 JavaScript 可以创建多个线程(也就是 workers)来并行处理任务。

Web Workers

在主线程中创建一个 Worker 时,浏览器会为其开辟一个新的线程。新的 Worker 线程独立于主线程,有自己的全局上下文,互不干扰,这样就可以在 Worker 线程中执行复杂的、耗时的代码,而不会阻塞主线程,从而提高页面的性能和响应速度。

关于 Web Workers 的主要特点:

  1. 并行执行:Web Workers 允许在后台线程中并行执行 JavaScript 代码,这样主线程就可以专注于用户交互和页面渲染,而不会被复杂的计算任务阻塞。

  2. 独立的运行环境:每个 Worker 都运行在自己的全局上下文中,这个上下文与主线程的上下文完全独立。

  3. 基于消息传递:主线程与 Worker 线程之间的通信是通过消息传递实现的。主线程和 Worker 线程不能直接访问彼此的全局变量,只能通过 postMessage 方法发送消息,通过 onmessage 事件接收消息。

  4. 限制:由于 Worker 线程与主线程是完全独立的,因此在 Worker 线程中有一些限制,例如不能直接操作 DOM,不能访问 window 和 document 对象,不能使用某些默认方法和属性等。

  5. Web Workers的线程数上限:上限主要取决于浏览器和硬件的限制。大多数现代浏览器都会对 Web Workers 的数量进行限制,以防止过多的线程消耗过多的系统资源。这个限制通常是动态的,取决于系统的可用资源。例如,Chrome 浏览器的 Web Workers 数量上限大约是每个域名120个。即使浏览器允许创建更多的 Web Workers,硬件资源(如CPU核心数)也可能会成为限制因素。如果创建的 Web Workers 数量超过CPU的核心数,那么这些 Web Workers 将需要在CPU核心之间进行切换,这可能会导致上下文切换的开销,降低性能。因此,虽然 Web Workers 可以帮助在后台线程中运行任务,但是仍然需要谨慎地使用它们,避免创建过多的 Web Workers。在大多数情况下,应该根据任务的性质和硬件的限制,合理地选择 Web Workers 的数量。

  6. 浏览器兼容性

前端多线程在开发中的应用总结

一个简单的 Web Workers 的示例

// 在主线程中创建一个 Worker
let worker = new Worker('worker.js');

// 向 Worker 发送消息
worker.postMessage('Hello, worker!');

// 接收 Worker 的消息
worker.onmessage = function(event) {
    console.log('Received message from worker: ' + event.data);
};

// 在 worker.js 中
self.onmessage = function(event) {
    console.log('Received message from main thread: ' + event.data);
    self.postMessage('Hello, main thread!');
};

例子中,在主线程中创建了一个 Worker,然后通过 postMessage 方法向 Worker 发送消息,通过 onmessage 事件接收 Worker 的消息。在 Worker 线程中,也通过 self.onmessage 事件接收主线程的消息,通过 self.postMessage 方法向主线程发送消息。

主线程中和 Worker 线程都可以使用postMessage方法 和 onmessage事件。

Worker构造函数

Worker对象本身并没有静态方法。它的主要方法和事件包括

  1. postMessage():向Worker线程发送消息。
  2. terminate():立即终止Worker线程。
  3. onmessage:事件处理器,当Worker线程发送消息时触发。
  4. onerror:事件处理器,当Worker线程中发生错误时触发。

Worker构造函数接受一个必需的参数,即要在Worker线程中运行的脚本的URL。此外,它还可以接受一个可选的参数,这是一个选项对象,可以包含以下属性:

  1. type:字符串,表示Worker的类型。可能的值包括"classic"(默认值)和"module"。如果设置为"module",则Worker将被当作ES6模块来加载,并且可以使用importexport语句。
  2. credentials:字符串,表示加载Worker脚本时的凭证模式。可能的值包括"omit""same-origin""include"。默认值是"same-origin"

一个使用选项对象的例子

// 创建一个新的 Worker 线程,将其类型设置为 "module"
let myWorker = new Worker('worker.js', { type: 'module' });

例子中,创建了一个新的Worker线程,这个线程会作为ES6模块来加载worker.js脚本。

importScripts()

在 Web Worker 中,可以使用 importScripts() 函数来引入一个或多个 JavaScript 脚本。这个函数接受一个或多个字符串参数,每个参数都是一个脚本的 URL。这些脚本会被同步加载,然后按照指定的顺序执行。

// 在 worker.js 中
importScripts('script1.js', 'script2.js');

// 然后就可以使用 script1.js 和 script2.js 中定义的函数和变量了

需要注意的是,importScripts() 是同步的,也就是说,它会阻塞 Worker 线程,直到所有的脚本都加载和执行完毕。因此,应该尽量减少使用 importScripts() 的次数,以避免不必要的阻塞。如果可能,应该一次性引入所有需要的脚本。

此外,由于同源策略的限制,只能引入与 Worker 脚本同源的脚本,除非这个脚本的服务器设置了适当的 CORS 头部。

self

在 Web Worker 中,self 是一个全局对象,它代表了 Worker 本身。可以使用 self 来访问和操作 Worker 的全局上下文。

  1. 发送和接收消息:可以使用 self.postMessage() 来向主线程发送消息,使用 self.onmessage 来接收主线程的消息。
self.onmessage = function(event) {
    var data = event.data;
    // 处理数据...
    self.postMessage(result);
};
  1. 引入脚本:可以使用 self.importScripts() 来引入一个或多个脚本。
self.importScripts('script1.js', 'script2.js');
  1. 关闭 Worker:可以使用 self.close() 来立即终止 Worker。
self.close();

前端多线程应用

复杂计算

网页需要进行大量的计算,比如图像处理、大数据分析等,这些计算可能会占用大量的CPU资源,导致用户界面卡顿。这时可以创建一个 Worker,在 Worker 中进行计算,这样就不会阻塞用户界面

假设有一个大的图像文件,需要对每个像素进行一些复杂的计算来应用一个滤镜。可以创建一个 Worker,在 Worker 中进行这种计算

// 在主线程中
var worker = new Worker('image-processing.js');
worker.postMessage(imageData);
worker.onmessage = function(event) {
    context.putImageData(event.data, 0, 0);
};

// 在 image-processing.js 中
self.onmessage = function(event) {
    var imageData = event.data;
    // 对 imageData 进行处理...
    self.postMessage(imageData);
};

网络请求

可以在 Worker 中进行网络请求,这样就不会阻塞用户界面。特别是需要进行大量的网络请求,或者需要处理大量的数据时,使用 Worker 会非常有用

// 在主线程中
var worker = new Worker('fetch-data.js');
worker.postMessage(urls);
worker.onmessage = function(event) {
    console.log('Received data: ' + event.data);
};

// 在 fetch-data.js 中
self.onmessage = function(event) {
    var urls = event.data;
    Promise.all(urls.map(url => fetch(url).then(response => response.json())))
        .then(data => self.postMessage(data));
};

定时任务

如果网页需要执行一些定时任务,比如每隔一段时间就需要更新一次数据,可以在 Worker 中使用setInterval或者setTimeout来执行这些任务。

假设需要每隔一分钟就检查一次新的邮件。可以创建一个 Worker,在 Worker 中使用setInterval来执行这个任务

// 在主线程中
var worker = new Worker('check-email.js');
worker.postMessage('john@example.com');
worker.onmessage = function(event) {
    console.log('New email: ' + event.data);
};

// 在 check-email.js 中
self.onmessage = function(event) {
    var email = event.data;
    setInterval(function() {
        fetch('https://api.example.com/emails?to=' + email)
            .then(response => response.json())
            .then(data => self.postMessage(data));
    }, 60000);
};

实际上,可以在任何需要在后台运行的任务中使用 Web Workers。只要记住, Worker 是运行在单独的线程中,不能访问 DOM,不能访问全局变量,只能通过消息传递来与主线程通信。

多线程一定比单线程速度快吗

多线程一定比单线程速度快吗?当然是不一定。

多线程的速度取决于很多因素,包括任务的性质、线程的数量、线程间通信的开销等。

举一个简单的例子,来说明多线程并不一定比单线程快

// 单线程计算斐波那契数列
function fibonacci(n) {
    return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
}
console.time('Single-threaded Fibonacci');
console.log(fibonacci(40));
console.timeEnd('Single-threaded Fibonacci');

// 多线程计算斐波那契数列
var worker = new Worker(URL.createObjectURL(new Blob([`
    self.onmessage = function(event) {
        var n = event.data;
        postMessage(fibonacci(n));
    };
    function fibonacci(n) {
        return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
    }
`], { type: 'text/javascript' })));
worker.onmessage = function(event) {
    console.log(event.data);
    console.timeEnd('Multi-threaded Fibonacci');
};
console.time('Multi-threaded Fibonacci');
worker.postMessage(40);

例子中,分别使用单线程和多线程计算斐波那契数列的第40项。会发现,多线程版本并不比单线程版本快,甚至可能更慢。

这是因为创建 worker、发送和接收消息都需要时间。而且由于 JavaScript 的单线程特性,worker 之间不能共享内存,每次通信都需要复制数据,这也会带来额外的开销。因此,对于这种计算密集型的任务,多线程并不会带来性能的提升。

总的来说,是否使用多线程,以及如何使用多线程,需要根据具体的应用场景和需求来决定。

对于 IO 密集型的任务,或者可以并行处理的大数据任务,多线程可能会有很大的帮助。但是对于 CPU 密集型的任务,特别是那些无法并行处理的任务,多线程可能并不会带来性能的提升,甚至可能会降低性能。

总结一下

  1. HTML5 引入了 Web Workers API,使得 JavaScript 可以创建多个线程(也就是 workers)来并行处理任务
  2. Web Workers 的主要特点:并行执行、独立的运行环境、基于消息传递、限制、Web Workers 的线程数上限
  3. Worker对象本身并没有静态方法。它的主要方法和事件包括
    1. postMessage():向Worker线程发送消息。
    2. terminate():立即终止Worker线程。
    3. onmessage:事件处理器,当Worker线程发送消息时触发。
    4. onerror:事件处理器,当Worker线程中发生错误时触发。
  4. 注意 Worker 还有一个可选参数
  5. 主线程和 Worker 线程都通过postMessage方法向彼此发送消息,通过onmessage事件接收的消息
  6. Worker 中importScripts()的使用:引入脚本,受到同源策略限制
  7. Worker 中使用selfself代表 Worker 本身
  8. Worker 可以用在复杂计算、网络请求、定时任务中
  9. 多线程不一定比单线程快
  10. 浏览器依然对 Web Workers 并发数有限制,所以在单域名下6个并发请求可能是某种需求的速度的上限了

看看负责的项目哪里可以用 Web Workers 优化。

本文完。

参考文献

Worker()

转载自:https://juejin.cn/post/7281196961751695421
评论
请登录