赞
踩
JavaScript
为什么要保证单线程呢?
DOM
,可能会导致不可预测的行为或冲突。随着Web
应用复杂度增加,特别是对于CPU
密集型任务、大量I/O
操作的需求,多线程或并发处理变得尤为重要。
其实呢我们对于提高性能方面呢,也引入了很多种方法。
比如说引入了异步的概念,引入了Promise,async/await
等等。当然这里也引入了前端的多线程。
它是HTML5
中引入的一个API
,它允许在浏览器环境中运行独立于主UI
线程的后台线程。都已经独立了,说明开发者可以在不阻塞用户界面的情况下执行耗时的计算或其他密集型任务。
Web Workers
通过Worker
构造函数创建,这个构造函数接受一个脚本文件的URL作为参数。
在Worker
线程中执行的代码位于这个脚本文件内。一旦线程被创建了,它相当于就独立了,并且它就运行在一个隔离的环境中,所以这里要注意它是不能直接访问DOM
的,但可以通过消息传递与主线程通信。
Worker
线程之间的通信:postMessage
方法,但是主线程中的参数一般都会有task
和data
。task
指它的类型,Worker
可以根据task
去执行相关的代码。Worker
线程执行完了之后,那就直接通过postMessage
方法发送结果回主线程呗。Web Workers
非常适合以下几种场景:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>多线程案例</title> </head> <body> <button onclick="calculateWithWorker()">开始计算</button> <script> function calculateWithWorker() { console.log('----------------多线程-----------------'); let start = new Date() const worker = new Worker('worker.js'); const largeArray = Array.from({ length: 1000000 }, () => Math.random()); worker.postMessage(largeArray); worker.onmessage = (e) => { let end = new Date() console.log(`耗时: ${end - start}ms`); }; console.log('我爱吃肉夹馍(多线程)'); console.log('----------------单线程-----------------'); const startTime = performance.now(); const largeArray1 = Array.from({ length: 1000000 }, () => Math.random()); // 长时间运行的计算 const result = largeArray1.reduce((acc, curr) => acc + curr, 0); const endTime = performance.now(); console.log(`耗时: ${endTime - startTime}ms`); console.log('我爱吃肉夹馍(单线程)'); } </script> </body> </html>
self.addEventListener('message', (event) => {
const array = event.data;
/**
* ok, 这里我简单的说一下reduce方法,reduce方法里面有很多参数,reduce的返回值有很多种类型,数组,对象,一个变量都是可以的。
* 首先第一个参数是一个callback,这个函数执行的是你想要做操作的过程。
* 第二个参数是初始值,这个初始值是可选的,如果不传的话,那么reduce方法会从数组的第一个元素开始执行。
*/
const result = array.reduce((acc, curr) => acc + curr, 0);
postMessage(result);
}, false);
当我们分开运行我们会发现:
这里插一嘴,我是用的
http-server C:\文件夹\玩转多线程\
这个命令进行测试的,你如果没有。
请去npm install http-server
一下,然后可以在页面的Console
观察运行结果
单线程:
多线程:
我们发现打印出来是不一样的,通过代码分析我们可以得出结论就是在多线程的情况之下,是不会影响主线程的运行的,所以"我爱吃肉夹馍(多线程)"
会出现在耗时的打印前面;
但是相较于单线程的情况,我们会发现"我爱吃肉夹馍(单线程)"
会出现在耗时的打印后面,因为单线程就算大量运算也是会等它运算完成才会执行后面的代码的。
既然都叫共享工作线程了,那么它肯定不是被一个脚本所持有的线程。
Shared Workers
是 Web Workers
的一个变种,它允许同一域名下的多个脚本(包括不同窗口,标签页,iframe
中的脚本)访问同一个Worker
实例。
所以它的资源是被多个上下文持有,而不需要每一个上下文都去创建和维护自己的Worker
实例。
Shared Workers
通过SharedWorker
构造函数创建,它的工作方式与普通的Web Worker
类似,但是增加了跨上下文通信的能力。
共享状态:多个脚本可以共享一个线程,这意味着它们可以访问相同的变量和函数,这在需要全局共享数据或服务时特别有用。
生命周期管理:对于Shared Workers
来说,它的生命周期肯定不会依赖创建它的线程,那就表明即使创建它的线程关闭了它也不会关闭。
Shared Worker
只有在以下情况下会被终止:
Shared Worker
对象的 terminate()
方法。port
与之通信,也就是说,当最后一个与其通信的脚本或页面关闭或断开了连接时,Shared Worker
将会被关闭。Shared Worker
只能被遵守同源策略的脚本访问。同源策略其实就是Web安全的一个核心原则,只有当两个资源拥有相同的协议、域名和端口号时,它们才被视为来自同一个源,可以相互访问对方的资源。举一个例子:
http://example.com/path1
http://example.com/path2
(路径不同,但协议、域名和端口号相同)https://example.com/path2
(协议不同)http://subdomain.example.com/path3
(域名不同)http://example.com:8080/path4
(端口号不同)Shared Worker
可以作为一个中心点来协作数据的分发和更新。Shared Worker
可以减少冗余的网络请求和资源消耗。Shared Worker
可以作为缓存层,存储频繁访问的数据,或者预加载资源。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>玩转共享工作线程1</title> </head> <body> <script> var sw = new SharedWorker('sharedWorker.js'); sw.port.start(); sw.port.onmessage = function (event) { console.log('线程1接受到消息为:', event.data); }; sw.port.postMessage('ping'); </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>玩转共享工作线程2</title> </head> <body> <script> var sw = new SharedWorker('sharedWorker.js'); sw.port.start(); sw.port.onmessage = function (event) { console.log('线程2接受到消息为:', event.data); }; sw.port.postMessage('ping'); </script> </body> </html>
/** * self.onconnect 事件处理器用于处理每当有新的脚本(如来自另一个页面或 iframe 的脚本)连接到 Shared Worker 时触发的事件 */ self.onconnect = function (e) { // e.ports 是一个 MessagePort 对象的数组,它代表了与连接到 Shared Worker 的脚本之间的通信渠道。 // 通常,e.ports 数组只包含一个元素,因为每个连接建立一个单独的 MessagePort。 // 其实上面的意思简而言之就是创建了一个与连接脚本通信的端口。通过这个端口,Shared Worker 能够接收和发送消息,实现与连接脚本之间的数据交换。 var port = e.ports[0]; port.start(); port.onmessage = function (e) { if (e.data === 'ping') { port.postMessage('pong'); } }; };
顾名思义服务工作线程,既然都是服务的线程了,那就说明它肯定不会Web Worker
和 Shared Worker
那样显式的调用执行,它是一种运行在浏览器背后的脚本。
Service Workers
不仅可以缓存静态资源,如HTML、CSS
和JavaScrip
t文件,还可以缓存动态内容如API
响应,所以它能实现离线的服务。
离线访问:通过缓存关键资源,Service Workers可以使Web应用在离线状态下依然可用。
请求拦截:Service Workers可以拦截和修改网络请求,允许开发者控制请求的响应,如从缓存中读取数据或执行网络请求。
推送通知:Service Workers可以注册接收推送通知,使得Web应用能够像原生应用一样接收并显示通知。
这个推送通知是什么意思呢?
意思就是它允许服务器主动向客户端发送消息,即使用户没有打开或正在使用该Web应用。这种功能通常用于即时通讯、电子邮件提醒、新闻更新、促销信息等场景,以吸引用户重新访问网站或Web应用。
Service Workers
通过缓存和离线访问功能,提高了应用的可靠性和响应速度,即使在网络状况不佳时,用户仍能访问应用的关键功能。下面是一个简单的Service Worker
示例,它展示了如何注册一个Service Worker
并设置基本的缓存策略。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>玩转服务工作线程</title> </head> <body> <script> console.log('serviceWorker in navigator', navigator); if ('serviceWorker' in navigator) { window.addEventListener('load', function () { navigator.serviceWorker.register('/serviceWorker.js') .then(function (registration) { console.log('注册成功:', registration.scope); }, function (err) { console.log('注册失败:', err); }); }); } </script> </body> </html>
// 这一步操作是为了将资源缓存到本地,以便在离线时也能访问 self.addEventListener('install', function (event) { // Service Worker主线程需要等到所有资源都缓存完成之后才能安装成功即等待指定的Promise完成 event.waitUntil( // 用于打开或者创建一块名为'my-cahe-v1'的缓存空间。此方法返回一个 Promise,当缓存空间创建成功后,Promise会被resolve // 并返回一个 Cache 对象,这个对象提供了对缓存的访问 caches.open('my-cahe-v1').then(function (cache) { return cache.addAll(['/', 'serviceWorker.js', 'styles.css']); }) ); console.log('Service Worker 安装成功'); }); // 这一步操作是为了在用户访问资源时,优先从缓存中获取资源,如果缓存中没有,则从网络中获取 self.addEventListener('fetch', function (event) { console.log('Service Worker 拦截到请求'); // caches.match 方法用于查找缓存中是否存在与当前请求相匹配的响应。如果找到,它会返回一个包含响应的Promise。 event.respondWith(caches.match(event.request).then(function (response) { // 如果在缓存中找到了响应,则返回该响应,否则返回网络请求的响应 return response || fetch(event.request); }) ); });
当使用上面的测试方法我发现有问题,结果查阅资料才知道Service Worker
只能在 HTTPS
上注册,除非你正在本地开发环境下使用 localhost
或者特定的本地 IP
地址。这意味着,如果你的站点在 HTTP
下运行,Service Worker
将无法注册。
SharedArrayBuffer
是JavaScript
中一种特殊类型的ArrayBuffer
,它允许被多个Web Workers
或 JavaScript
线程共享和访问。
是JavaScript
中的一个内置对象,它表示一块固定大小的二进制数据缓冲区。ArrayBuffer
是底层的数据结构,可以存储不同类型的数据,如整数、浮点数或其他二进制数据。
ArrayBuffer
的大小在创建时确定,并且之后不能更改。JavaScript
提供了多种类型的视图用于去访问缓冲区中的数据,比如说Int8Array
, Uint8Array
, Int16Array
, Float32Array
等。既然SharedArrayBuffer
是JavaScript
中一种特殊类型的ArrayBuffer
,那么它的创建方式跟ArrayBuffer
是一样的,比如说:
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 4);
const i32a = new Int32Array(sab);
顾名思义原子操作,它是一组静态的方法(那么它的调用直接就是Atomics.xxx(...)
),提供了一系列的方法用于对 SharedArrayBuffer
中的数据执行原子操作。
原子操作就是在执行过程中不会被其他的线程中断的操作,从而避免了竞态条件(就是可能会因为多个线程都在操作时从而产生意想不到的结果比如说一个线程在修改某一个值的时候接着发送中断,另外一个线程读取这个值的可能是修改之前的值,发现读到的是脏数据)。
常见的 Atomics
方法:
想要做并发控制,可以实现的方式有很多:
互斥锁是一种常见的同步机制,多个线程通过维护一个共同的锁资源来控制对共享资源的访问,确保任何时刻只有一个线程可以访问该资源。
下面是一个简单的互斥锁实现示例,通过 Atomics
提供的方法:
function mutexLock(buffer) {
const lock = new Int32Array(buffer);
while (Atomics.compareExchange(lock, 0, 0, 1) !== 0) {}
}
function mutexUnlock(buffer) {
const lock = new Int32Array(buffer);
Atomics.store(lock, 0, 0);
}
你看懂了吗?那我想提几个问题:
Atomics.compareExchange(lock, 0, 0, 1)
使用方法是怎么控制的?解答:
Atomics.compareExchange
方法:
Atomics.compareExchange(view, index, expectedValue, newValue)
Int32Array
、Uint32Array
等类型,用于访问 SharedArrayBuffer
。方法的行为:
view[index]
是否等于 expectedValue
。view[index] === expectedValue
view[index]
设置为 newValue
。expectedValue
。view[index] !== expectedValue
view[index]
的当前值。上面也说过ArrayBuffer
维护的其实是一块数据缓冲区。所以实际上,在每次调用 mutexLock
和 mutexUnlock
时创建 Int32Array
视图并不是创建新的锁实例,而是创建了一个指向同一个 SharedArrayBuffer
的视图。这意味着每次创建的 Int32Array
都指向相同的内存区域,并且共享相同的数据。
执行原子操作,确保整个操作不可分割。在 JavaScript
中,Atomics
提供了一系列原子操作方法。
信号量是一种更复杂的同步机制,它可以控制多个线程对资源的访问。
当说到性能的成本来说,对于多线程是一个非常重要的指标,因为过多的线程肯定会导致性能的下降,所以对于多个线程的性能层面,我们需要考虑以下多个方面:
navigator.hardwareConcurrency
获取系统的物理核心数,但是这个核心数只是一个近似值,并不总是准确的。navigator
:它是JavaScript
中的一个全局对象,它提供了有关浏览器的信息。Worker
因为一直在运行导致过载而其它的Worker
空闲。Worker
完成了一个任务之后,这个队列就会取出一个任务分配给一个Worker
。Worker
的 CPU
使用率和内存消耗情况。 Worker
的数量或暂停接收新任务。Worker
池
Worker
池,并在任务完成后将 Worker
放回池中。Worker
,减少资源开销。Worker
池:const workerPool = []; // 创建 Worker 池 function createWorkerPool(size) { for (let i = 0; i < size; i++) { const worker = new Worker('worker.js'); worker.busy = false; workerPool.push(worker); } } // 检查是否有空闲的 Worker,如果有则返回,否则返回 null function getAvailableWorker() { return workerPool.find(worker => !worker.busy); } // 将 Worker 放回 Worker 池 function releaseWorker(worker) { worker.busy = false; } // 处理下一个任务 function processNextTask() { const worker = getAvailableWorker(); if (worker) { // 执行任务 worker.postMessage(task); worker.busy = true; } } // 监听 Worker 的消息 workerPool.forEach(worker => { worker.onmessage = function (event) { releaseWorker(worker); // 释放 Worker,使其可用于接收下一个任务 // 处理下一个任务 processNextTask(); }; });
上面是对线程本身的性能思考,肯定也需要思考线程通信的成本。下面是一些减少通信成本的策略:
Transferable Objects
:这个方法就是使用 ArrayBuffer
和 Transferable Objects
(如 TypedArray
)来传输大量数据,因为它们支持 transfer 机制,可以直接将数据的所有权从主线程转移到 Worker,而不需要复制数据,但是这个方法也有需要注意的点,下面我会详细介绍一下这个方法。Worker
的处理效率。下面我来讲一下使用Transferable Objects
来降低通信成本的示例:
/** * 关于ArrayBuffer,我上面也介绍过,就不多赘述了。 * Transferable Objects:这个对象包括ArrayBuffer和基于ArrayBuffer 的视图(如 Int32Array, Float64Array 等)。 * transfer机制:当你使用 postMessage 方法将 Transferable Objects 传递给 Worker 时,你可以选择使用 transfer 机制。 * 它允许你在发送数据时直接转移数据的所有权,而不是复制数据。 */ // 创建 ArrayBuffer const bufferSize = 1024 * 1024; // 1MB const buffer = new ArrayBuffer(bufferSize); const data = new Float64Array(buffer); // 填充数据 for (let i = 0; i < data.length; i++) { data[i] = Math.random(); } // 创建 Worker const worker = new Worker('worker.js'); // 传递数据 worker.postMessage(data, [buffer]); // 将 buffer 作为可转移对象传递
注意事项:
Worker
时,你不能再在主线程中使用这个 ArrayBuffer
,因为它的所有权已经被转移了。ArrayBuffer
和基于 ArrayBuffer
的视图,不能用于普通的 JavaScript
对象。总结:这个方法大大地降低了通信成本,但是需要确保正确地管理 ArrayBuffer
的所有权,并在传递后不再使用原始对象。
Web Workers
运行在主线程的沙箱环境中,这意味着它们无法访问主线程的 DOM 和其他全局对象。Web Workers
的执行不会影响主线程,从而提高了应用的安全性。Worker
与主线程之间的通信只能通过消息传递的方式进行。Web Workers
的沙箱环境有助于防止恶意脚本影响主线程或其他 Worker
。多数现代浏览器都支持 Web Workers
,但还是有一些兼容性和限制需要注意。
Worker
的数量或大小有限制。Web Workers API
,例如 Shared Workers
或 Service Workers
的某些特性。Web Workers
必须遵守同源策略,这意味着它们只能加载来自同一来源的脚本和资源。在一个图像编辑应用中,需要对大量图像进行批处理,如缩放、裁剪或应用滤镜效果。
使用 Web Workers
来并行处理图像,每张图像在一个单独的 Worker
中处理。
下面是一个简单的示例,展示如何使用 Web Workers
来并行处理图像数据:
主线程:
const worker = new Worker('imageProcessor.js');
function processImage(imageData) {
worker.postMessage(imageData);
worker.onmessage = function (event) {
const processedImageData = event.data;
// 在主线程中处理处理后的图像数据
};
}
// 调用 processImage 函数处理图像数据
**Worker 线程 **:
self.onmessage = function (event) {
const imageData = event.data;
// 对 imageData 进行处理
const processedImageData = /* 处理后的图像数据 */
self.postMessage(processedImageData);
};
Web Workers
的主要目的是在后台执行任务,避免阻塞主线程。因此,应尽量将耗时的计算任务放在 Worker
中执行。postMessage
:postMessage
是 Worker
与主线程通信的主要方式,应尽量减少使用频率,避免频繁的消息传递带来的性能开销。Worker
与主线程之间传输大量数据时,应尽量使用 Transferable Objects
,以减少数据复制的开销。主线程:
负责执行 JavaScript
代码,处理用户交互,以及渲染页面。
对于JavaScript
的多线程的话,我们介绍了三种类型:
JavaScript
代码,不会阻塞主线程。iframe
)之间共享,实现更复杂的并发处理。JavaScript
的多线程的优势:
JavaScript
的多线程的挑战:
Web Workers
,需要考虑兼容性问题。通过查阅资料我发现未来WebAssembly
将会大放异彩:
WebAssembly (Wasm)
是一种低级编译目标,它能够为 Web
提供高性能计算能力。WebAssembly
的发展,越来越多的高性能库和框架将会出现,这将使得 JavaScript
应用程序能够执行更为复杂的计算任务。WebAssembly
已经支持多线程,开发人员可以利用多核处理器的全部潜力来加速计算密集型任务。Web
应用程序可能可以通过 Wasm
运行更高效的多线程代码。当然随着人工智能的发展,Web
平台上面的机器学习和AI
应用将会变得越来越普遍,多线程和并行处理也将成为关键组成部分。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。