当前位置:   article > 正文

玩转JavaScript多线程

玩转JavaScript多线程

1. 引言

1.1 JavaScript的单线程本质:

JavaScript为什么要保证单线程呢?

  • 单线程确保了在任意时刻只有一个任务被执行,如果多个线程同时修改DOM,可能会导致不可预测的行为或冲突。
  • 单线程简化了编程模型,避免了多线程环境中出现的常见地竞争条件和死锁问题。

1.2 多线程的需求:

  • 随着Web应用复杂度增加,特别是对于CPU密集型任务、大量I/O操作的需求,多线程或并发处理变得尤为重要。

  • 其实呢我们对于提高性能方面呢,也引入了很多种方法。

  • 比如说引入了异步的概念,引入了Promise,async/await等等。当然这里也引入了前端的多线程。

2. Web Workers

2.1 概念与原理:

概念

它是HTML5中引入的一个API,它允许在浏览器环境中运行独立于主UI线程的后台线程。都已经独立了,说明开发者可以在不阻塞用户界面的情况下执行耗时的计算或其他密集型任务。

  • Web Workers通过Worker构造函数创建,这个构造函数接受一个脚本文件的URL作为参数。

  • Worker线程中执行的代码位于这个脚本文件内。一旦线程被创建了,它相当于就独立了,并且它就运行在一个隔离的环境中,所以这里要注意它是不能直接访问DOM的,但可以通过消息传递与主线程通信。

主线程和Worker线程之间的通信

  • 两个线程的通信直接通过postMessage方法,但是主线程中的参数一般都会有taskdatatask指它的类型,Worker可以根据task去执行相关的代码。
  • 当然当Worker线程执行完了之后,那就直接通过postMessage方法发送结果回主线程呗。

2.2 使用场景:

Web Workers非常适合以下几种场景:

  • 图像处理:如图像滤镜、图像识别或图像压缩,这些任务通常需要大量的CPU时间。
  • 大数据计算:数据分析、科学计算或统计分析,特别是那些需要处理大型数据集的任务。
  • 长时间运行的算法:如加密解密操作、路径规划算法、机器学习模型预测等。
  • 音频和视频处理:实时音频效果、视频编码或解码。
  • 游戏逻辑:游戏中的AI计算、物理引擎模拟等,可以独立于渲染线程执行。

2.3 实战示例:

  • 我们就来看看到底有什么区别

html:

<!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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

work.js:

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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

当我们分开运行我们会发现:

这里插一嘴,我是用的

http-server C:\文件夹\玩转多线程\这个命令进行测试的,你如果没有。

请去npm install http-server一下,然后可以在页面的Console观察运行结果

运行截图

  • 单线程:
    在这里插入图片描述

  • 多线程:

    在这里插入图片描述

  • 我们发现打印出来是不一样的,通过代码分析我们可以得出结论就是在多线程的情况之下,是不会影响主线程的运行的,所以"我爱吃肉夹馍(多线程)"会出现在耗时的打印前面;

  • 但是相较于单线程的情况,我们会发现"我爱吃肉夹馍(单线程)"会出现在耗时的打印后面,因为单线程就算大量运算也是会等它运算完成才会执行后面的代码的。

3. Shared Workers

3.1 共享工作线程:

概念:

既然都叫共享工作线程了,那么它肯定不是被一个脚本所持有的线程。

Shared WorkersWeb Workers的一个变种,它允许同一域名下的多个脚本(包括不同窗口,标签页,iframe中的脚本)访问同一个Worker实例。

所以它的资源是被多个上下文持有,而不需要每一个上下文都去创建和维护自己的Worker实例。

Shared Workers通过SharedWorker构造函数创建,它的工作方式与普通的Web Worker类似,但是增加了跨上下文通信的能力。

Shared Worker的特点:

  • 共享状态:多个脚本可以共享一个线程,这意味着它们可以访问相同的变量和函数,这在需要全局共享数据或服务时特别有用。

  • 生命周期管理:对于Shared Workers来说,它的生命周期肯定不会依赖创建它的线程,那就表明即使创建它的线程关闭了它也不会关闭。

    Shared Worker 只有在以下情况下会被终止:

    1. 明确调用了 Shared Worker 对象的 terminate() 方法。
    2. 没有任何页面或脚本通过其 port 与之通信,也就是说,当最后一个与其通信的脚本或页面关闭或断开了连接时,Shared Worker 将会被关闭。

使用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 (端口号不同)
  • 权限和安全性:这个安全问题肯定也是需要考虑的,因为我们使用的共享工作线程,我们就需要去考虑每个人是否有权限对这一块内容的操作以及持有相应的资源,防止脚本间的干扰和潜在的攻击是必不可少的。

3.2 应用场景:

  • 实时数据同步:需要在多个页面或标签页之间实时同步数据,比如说协作编辑文档、聊天应用等等,Shared Worker可以作为一个中心点来协作数据的分发和更新。
  • 服务共享:比如说很多用户都会订阅天气预报、新闻推送的服务,如果给每一个人都去创建一个线程,浪费资源,降低性能,使用Shared Worker可以减少冗余的网络请求和资源消耗。
  • 缓存和预加载Shared Worker可以作为缓存层,存储频繁访问的数据,或者预加载资源。

3.3 实战示例:

线程1

<!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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

线程2

<!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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

sharedWorker.js

/**
 * 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');
    }
  };
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

运行截图

在这里插入图片描述
在这里插入图片描述

4. Service Workers

4.1 离线与缓存:

顾名思义服务工作线程,既然都是服务的线程了,那就说明它肯定不会Web WorkerShared Worker那样显式的调用执行,它是一种运行在浏览器背后的脚本。

Service Workers 不仅可以缓存静态资源,如HTML、CSSJavaScript文件,还可以缓存动态内容如API响应,所以它能实现离线的服务。

4.2 功能与优势:

功能:

  1. 离线访问:通过缓存关键资源,Service Workers可以使Web应用在离线状态下依然可用。

  2. 请求拦截:Service Workers可以拦截和修改网络请求,允许开发者控制请求的响应,如从缓存中读取数据或执行网络请求。

  3. 推送通知:Service Workers可以注册接收推送通知,使得Web应用能够像原生应用一样接收并显示通知。

    • 这个推送通知是什么意思呢?

      意思就是它允许服务器主动向客户端发送消息,即使用户没有打开或正在使用该Web应用。这种功能通常用于即时通讯、电子邮件提醒、新闻更新、促销信息等场景,以吸引用户重新访问网站或Web应用。

优势:

  1. 增强用户体验Service Workers通过缓存和离线访问功能,提高了应用的可靠性和响应速度,即使在网络状况不佳时,用户仍能访问应用的关键功能。
  2. 减少服务器负担:通过缓存静态资源,减少了对服务器的请求,减轻了服务器的负担,同时也降低了带宽消耗。

4.3 实战示例:

下面是一个简单的Service Worker示例,它展示了如何注册一个Service Worker并设置基本的缓存策略

html:

<!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>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

serviceWorker.js:

// 这一步操作是为了将资源缓存到本地,以便在离线时也能访问
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);
  })
  );
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

当使用上面的测试方法我发现有问题,结果查阅资料才知道Service Worker 只能在 HTTPS 上注册,除非你正在本地开发环境下使用 localhost 或者特定的本地 IP 地址。这意味着,如果你的站点在 HTTP 下运行,Service Worker 将无法注册。

5. SharedArrayBuffer与Atomics

5.1 原子操作与共享内存:

SharedArrayBuffer (SAB):

SharedArrayBufferJavaScript中一种特殊类型的ArrayBuffer,它允许被多个Web WorkersJavaScript 线程共享和访问。

ArrayBuffer

JavaScript中的一个内置对象,它表示一块固定大小的二进制数据缓冲区。ArrayBuffer是底层的数据结构,可以存储不同类型的数据,如整数、浮点数或其他二进制数据。

  • ArrayBuffer 的大小在创建时确定,并且之后不能更改。
  • JavaScript提供了多种类型的视图用于去访问缓冲区中的数据,比如说Int8Array, Uint8Array, Int16Array, Float32Array 等。

既然SharedArrayBufferJavaScript中一种特殊类型的ArrayBuffer,那么它的创建方式跟ArrayBuffer是一样的,比如说:

const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 4);
const i32a = new Int32Array(sab);
  • 1
  • 2

Atomics

顾名思义原子操作,它是一组静态的方法(那么它的调用直接就是Atomics.xxx(...)),提供了一系列的方法用于对 SharedArrayBuffer 中的数据执行原子操作。

原子操作就是在执行过程中不会被其他的线程中断的操作,从而避免了竞态条件(就是可能会因为多个线程都在操作时从而产生意想不到的结果比如说一个线程在修改某一个值的时候接着发送中断,另外一个线程读取这个值的可能是修改之前的值,发现读到的是脏数据)。

常见的 Atomics 方法:

  • load: 读取一个值。
  • store: 写入一个值。
  • add: 在当前值上加一个数。
  • sub: 在当前值上减一个数。
  • and: 对当前值进行按位与操作。
  • or: 对当前值进行按位或操作。
  • xor: 对当前值进行按位异或操作。
  • exchange: 将值替换为新的值,并返回旧的值。
  • compareExchange: 如果当前值等于预期值,则用新值替换;否则返回 false。
  • fence: 创建一个内存屏障,确保所有先前的内存操作都已完成。

5.2 高级并发控制:

想要做并发控制,可以实现的方式有很多:

1.互斥锁 (Mutex):

互斥锁是一种常见的同步机制,多个线程通过维护一个共同的锁资源来控制对共享资源的访问,确保任何时刻只有一个线程可以访问该资源。

下面是一个简单的互斥锁实现示例,通过 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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

你看懂了吗?那我想提几个问题

  1. Atomics.compareExchange(lock, 0, 0, 1)使用方法是怎么控制的?
  2. 每次方法不都是new 的一个新的实例吗,这样的话还能控制互斥吗?

解答

  1. Atomics.compareExchange 方法:

    Atomics.compareExchange(view, index, expectedValue, newValue)

    • view: 一个视图对象,通常是 Int32ArrayUint32Array 等类型,用于访问 SharedArrayBuffer
    • index: 要操作的元素的索引位置。
    • expectedValue: 当前期望在给定索引位置上的值。
    • newValue: 如果当前值与预期值匹配,则要存储的新值。

    方法的行为:

    1. 检查预期值
      • 方法首先检查 view[index] 是否等于 expectedValue
    2. 根据结果采取行动
      • 如果view[index] === expectedValue
        • view[index] 设置为 newValue
        • 返回 expectedValue
      • 如果view[index] !== expectedValue
        • 不做任何修改。
        • 返回 view[index] 的当前值。
  2. 上面也说过ArrayBuffer维护的其实是一块数据缓冲区。所以实际上,在每次调用 mutexLockmutexUnlock 时创建 Int32Array 视图并不是创建新的锁实例,而是创建了一个指向同一个 SharedArrayBuffer 的视图。这意味着每次创建的 Int32Array 都指向相同的内存区域,并且共享相同的数据。

2.原子操作:

执行原子操作,确保整个操作不可分割。在 JavaScript 中,Atomics 提供了一系列原子操作方法。

3.信号量 (Semaphores):

信号量是一种更复杂的同步机制,它可以控制多个线程对资源的访问。

6. 性能考量与优化

6.1 资源管理:

当说到性能的成本来说,对于多线程是一个非常重要的指标,因为过多的线程肯定会导致性能的下降,所以对于多个线程的性能层面,我们需要考虑以下多个方面:

1. 线程数量:

  • 对于线程的数量,理想情况就是创建的所有线程与可用的核心相匹配,其实就是所有的硬件资源能够被充分使用。过多的 Worker 可能会导致上下文切换频繁,反而降低性能。
  • 可以使用navigator.hardwareConcurrency获取系统的物理核心数,但是这个核心数只是一个近似值,并不总是准确的。
  • 对于 navigator:它是JavaScript中的一个全局对象,它提供了有关浏览器的信息。

2. 负载均衡

  • 既然都在说均衡了,那肯定意思就是要保证每一个线程都能够公平地获取任务,不能让某些Worker因为一直在运行导致过载而其它的Worker空闲。
  • 可以使用相应的数据结构来存储待处理的任务,比如说使用队列,当一个Worker完成了一个任务之后,这个队列就会取出一个任务分配给一个Worker

3. 优先级调度

  • 对于优先级调度的方式,直接维护一个优先级队列来管理任务的执行顺序即可。高优先级的任务会被优先执行。

4. 资源限制

  • 可以定期检查 WorkerCPU 使用率和内存消耗情况。
  • 当资源接近极限时,可以考虑减少 Worker 的数量或暂停接收新任务。

5. 生命周期管理

  • 创建 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();
  };
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

6.2 通信成本:

上面是对线程本身的性能思考,肯定也需要思考线程通信的成本。下面是一些减少通信成本的策略:

  1. 批量通信:意思就是减少通信次数,提高效率。避免频繁去发送小信息带来一些额外的开销。
  2. 数据压缩:意思就是减少数据量,提高传输速度。这种方式适合大数据集。可以使用一些压缩算法(如 LZ77、LZ78、Deflate 等)。
  3. 使用 Transferable Objects:这个方法就是使用 ArrayBufferTransferable Objects(如 TypedArray)来传输大量数据,因为它们支持 transfer 机制,可以直接将数据的所有权从主线程转移到 Worker,而不需要复制数据,但是这个方法也有需要注意的点,下面我会详细介绍一下这个方法。
  4. 缓存数据:意思就是减少向主线程请求数据的需求,提高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 作为可转移对象传递
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    注意事项

    1. 当你使用这个方法将你的所有权转移给 Worker 时,你不能再在主线程中使用这个 ArrayBuffer,因为它的所有权已经被转移了。
    2. 这个方法只能用于 ArrayBuffer 和基于 ArrayBuffer 的视图,不能用于普通的 JavaScript 对象。
    3. 因此在传递之前,应该断开所有其他视图的引用。

    总结:这个方法大大地降低了通信成本,但是需要确保正确地管理 ArrayBuffer 的所有权,并在传递后不再使用原始对象。

7. 安全与限制

7.1 沙箱机制:

  • Web Workers 运行在主线程的沙箱环境中,这意味着它们无法访问主线程的 DOM 和其他全局对象。
  • 这种沙箱机制确保了 Web Workers 的执行不会影响主线程,从而提高了应用的安全性。
  • Worker 与主线程之间的通信只能通过消息传递的方式进行。
  • Web Workers沙箱环境有助于防止恶意脚本影响主线程或其他 Worker

7.2 兼容性与限制:

多数现代浏览器都支持 Web Workers,但还是有一些兼容性和限制需要注意。

  • IE9 及以下版本不支持。
  • 一些浏览器可能对 Worker 的数量或大小有限制。
  • 一些浏览器可能不支持最新的 Web Workers API,例如 Shared WorkersService Workers 的某些特性。
  • Web Workers 必须遵守同源策略,这意味着它们只能加载来自同一来源的脚本和资源。

8. 实践案例与最佳实践

8.1 案例分析:

在一个图像编辑应用中,需要对大量图像进行批处理,如缩放、裁剪或应用滤镜效果。
使用 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 函数处理图像数据
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

**Worker 线程 **:

self.onmessage = function (event) {
  const imageData = event.data;
  // 对 imageData 进行处理
  const processedImageData = /* 处理后的图像数据 */
    self.postMessage(processedImageData);
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

8.2 开发建议:

  • 避免阻塞主线程Web Workers 的主要目的是在后台执行任务,避免阻塞主线程。因此,应尽量将耗时的计算任务放在 Worker 中执行。
  • 合理使用 postMessagepostMessageWorker 与主线程通信的主要方式,应尽量减少使用频率,避免频繁的消息传递带来的性能开销。
  • 优化数据传输:在 Worker 与主线程之间传输大量数据时,应尽量使用 Transferable Objects,以减少数据复制的开销。

9. 结论与展望

9.1 总结:

  • 主线程

    负责执行 JavaScript 代码,处理用户交互,以及渲染页面。

  • 对于JavaScript的多线程的话,我们介绍了三种类型:

    • Web Workers:可以在后台执行 JavaScript 代码,不会阻塞主线程。
    • Shared Workers:可以在多个浏览器上下文(如多个标签页或 iframe)之间共享,实现更复杂的并发处理。
    • Service Workers:用于在用户与网站交互时,提供离线缓存、后台同步、拦截网络请求等功能。
  • JavaScript的多线程的优势:

    • 提高性能:通过在后台执行任务,可以避免阻塞主线程,提高应用的响应速度。
    • 实现并发处理:可以同时处理多个任务,提高应用的并发能力。
    • 提高用户体验:通过在后台执行任务,可以提供更流畅的用户体验,如实时预览、后台数据同步等。
  • JavaScript的多线程的挑战:

    • 复杂性:多线程编程比单线程编程更复杂,需要处理线程同步、线程安全等问题。
    • 兼容性:并非所有浏览器都支持 Web Workers,需要考虑兼容性问题。
    • 调试困难:多线程程序的调试比单线程程序更困难,需要使用专门的工具和技术。

9.2 未来趋势:

通过查阅资料我发现未来WebAssembly将会大放异彩:

  • 高性能计算:
    • WebAssembly (Wasm) 是一种低级编译目标,它能够为 Web 提供高性能计算能力。
    • 随着 WebAssembly 的发展,越来越多的高性能库和框架将会出现,这将使得 JavaScript 应用程序能够执行更为复杂的计算任务。
  • 多线程支持:
    • WebAssembly 已经支持多线程,开发人员可以利用多核处理器的全部潜力来加速计算密集型任务。
    • 未来的 Web 应用程序可能可以通过 Wasm 运行更高效的多线程代码。

当然随着人工智能的发展,Web平台上面的机器学习和AI应用将会变得越来越普遍,多线程和并行处理也将成为关键组成部分。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Li_阴宅/article/detail/987976
推荐阅读
相关标签
  

闽ICP备14008679号