Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

性能优化:H5的线程池 #83

Open
wuyunqiang opened this issue Oct 23, 2024 · 0 comments
Open

性能优化:H5的线程池 #83

wuyunqiang opened this issue Oct 23, 2024 · 0 comments

Comments

@wuyunqiang
Copy link
Owner

先有问题再有答案

  1. js不是单线程嘛 为什么又说多线程?
  2. 如何理解H5的多线程?
  3. 如何理解h5的worker?
  4. web worker 有什么特点?
  5. 有什么使用场景?
  6. 有什么限制?
  7. 多线程间是如何通讯?

背景

JavaScript 是单线程运行的,这意味着同一个时间只能做一件事情。由于用户交互、网络请求、IO操作等可能会产生阻塞,所以 JavaScript 引入了异步机制和事件循环的概念。

单线程&异步

异步可以看做是单线程面对需要大量耗时操作时的一种解决方案,它将这些耗时操作异步处理,也就是挂起这些事件,进行下一步的操作。当异步事件处理完成后,再通过回调函数的方式进行处理。所以,异步并不是在单线程之外开辟了新的线程,而是通过事件挂起和回调函数的方式使得 JavaScript 引擎不需要阻塞等待,可以先处理其他任务。

单线程&事件循环

事件循环是 JavaScript 实现异步的一种方法,它可以解决 JavaScript 单线程同步执行带来的阻塞问题。事件循环的机制是,将所有的任务分为宏任务和微任务,JavaScript 引擎先执行一个宏任务,紧接着执行所有的微任务,然后再执行下一个宏任务。这种模式不断循环,称为事件循环

单线程&多线程

JavaScript是单线程的,这是因为JavaScript设计初衷是处理简单的DOM操作,复杂和耗时的操作可能导致页面阻塞,影响用户体验。

然而,随着Web应用变得越来越复杂,JavaScript也需要处理更多的复杂且计算密集型任务。为了解决这个问题,HTML5提出了Web Workers标准,允许JavaScript创建多个工作线程,这些工作线程在后台执行任务,不会影响主线程和用户界面的相应。

我们可以认为,JavaScript的主线程仍然是单线程的,但是通过Web Workers,JavaScript能够创建生成真正的操作系统级别的工作线程,实现多线程编程

web worker兼容性

截屏2024-08-05 下午7.16.01.png
线上完全可用 无兼容性问题。

使用场景

cpu密集型的任务

如果有耗时任务 在主线程中长时间占用cpu,这样的计算可能会阻塞用户界面卡顿,导致无法响应用户的操作。

测试代码如下:

const test = () => {
  let count = 0;
  for (let i = 0; i < 1000000000; i++) {
    count = count + i;
  }
};
test();


运行在主线程的任务性能分析如图:

主线程耗时12s左右 这期间左侧列表是卡死 不能响应用户滚动的
截屏2024-08-05 下午7.59.35.png

运行在worker线程

性能分析如图-1

可以看到当选中主线程时 js执行仅3毫秒
截屏2024-08-05 下午8.01.20.png

性能分析如图-2

当选中worker线程时 js执行耗时13s

截屏2024-08-05 下午8.01.33.png

运行在worker中的整个过程 左侧列表是可以响应用户滚动的, 这就是多线程的魅力 开启了一个工作线程 分担了主线程的耗时任务 有效的避免了卡顿。

周期性后台任务

这部分一般不需要用户交互,但需要定时执行的任务。这类任务通常包括一些定时获取服务器数据、清理缓存、自动保存用户操作等。
例如高精准大定时器功能,举例可以参考这篇文章 时间系列二:高精度计时器方案,worker计时,settimeout差值补偿,raf计时器,setInterval

使用限制

同源限制:

Web Workers 只能加载来自同源的脚本。也就是说,你不能在你的网站上使用一个来自其他域的脚本,这是出于安全原因。

不能访问 DOM

Web Workers 不能访问页面的 DOM。这是因为 DOM 操作不能用于多线程环境。目的是防止出现两个线程对 DOM 进行修改而造成的数据不同步。

不能访问某些对象:

Web Workers 不能访问window对象、document对象、parent对象等, 即使Web Workers不能访问这些对象,它们仍然可以发起AJAX请求或使用 WebSockets 这类网络API。 查看worker可以使用的api

使用方式

创建worker

const worker = new Worker('worker.js', {
  type: 'module',
  credentials: 'same-origin'
});

Worker构造函数接收两个参数:

  1. url: 这是一个字符串值,表示一个指向 JavaScript 文件的 URL。这个参数是必须的,它的值应该是一个相对于当前 HTML 文档的相对路径,或者是一个完整的 URL。

  2. options: 这是一个对象,用于配置新创建的 Worker 实例。这个参数是可选的,可以包含以下属性:

  • type: 用于指定要创建的 Worker 的类型。支持的值有 "classic" 和 "module"。"classic" 表示 Worker 脚本是一个普通 JavaScript 文件,这是默认类型。"module" 表示 Worker 脚本是一个 JavaScript 模块。
  • credentials: 用于指定在加载 Worker 脚本时是否需要证书(cookies)。支持的值有 "omit"、"same-origin" 和 "include"。

Blob对象创建worker

// 创建一个 Blob 对象
const blob = new Blob([
  "onmessage = function(e) { postMessage('Worker: ' + e.data); }"
]);
// 创建一个 Blob URL
const blobURL = window.URL.createObjectURL(blob);
// 使用 Blob URL 创建一个 Worker
const worker = new Worker(blobURL);
worker.onmessage = function(e) {
  console.log(e.data);  // 输出 "Worker: Hello"
}
worker.postMessage('Hello');  // 向 worker 发送消息

多线程间通讯

Worker 线程和主线程不能直接共享内存或变量,它们之间的通信必须通过消息传递来完成。可以使用 postMessage 方法来发送消息,并在另一边使用 onmessage 事件监听器来接收消息。

worker.onmessage = function(e) {
  console.log(e.data);  // 输出 "Worker: Hello"
}
worker.postMessage('Hello');  // 向 worker 发送消息

实现一个线程池

本文提供一个通过web worker实现线程池的能力。

demo

export const workerStr = `
this.store = {};
this.addEventListener(
  "message",
  function (e) {
    var key = e.data.key;
    if(e.data.type === 'remove-item' && this.store[key]){
      this.store[key] = null;
      return;
    }
    if(e.data.type === 'remove'){
      this.store = {};
      return;
    }
    if (!e.data.task) {
      return;
    }
    var task = new Function('return (' + e.data.task + ')(this.store)');
    Promise.resolve(task())
      .then((res) => {
        this.store[key] = {
          status: "succ",
          data: res,
        };
      })
      .catch((error) => {
        this.store[key] = {
          status: "fail",
          data: error,
        };
      })
      .finally(() => {
        console.log('test worker res', {
          key,
          ...this.store[key],
        })
        this.postMessage({
          key,
          ...this.store[key],
        });
      });
  },
  false
);
`;

export default workerStr;
import { PoolList, Resolve, TaskRes } from '../type/task';
import workerStr from './worker'
let blob: Blob;
let url: string;
let threadPool: Worker[] = [];

/**
 * 使用webworker开启一个线程池 执行复杂运算
 * real parallel
 * @param list 任务列表
 * @param max 最大并发数 默认为2 最好和cpu数量一致 不要设置过多
 * @returns 返回一个promise
 */
export const pool = async (list: PoolList, max = 1) => {
  if (!blob) {
    blob = new Blob([workerStr], { type: 'text/plain' });
  }
  if (!url) {
    url = URL.createObjectURL(blob);
  }
  const store: { [key: string]: TaskRes } = {}
  let _resolve: Resolve;
  const p = new Promise((resolve) => {
    _resolve = resolve;
  })
  let doneCount = 0;
  let index = 0;
  const runner = (curWorker: Worker) => {
    const curIndex = index++;
    if (!curWorker || !list[curIndex]) {
      return;
    }
    const { key, task } = list[curIndex];
    curWorker.postMessage({
      key,
      task: `${task}`
    });
    curWorker.onmessage = function (event) {
      store[event.data.key] = event.data;
      doneCount++;
      if (doneCount === list.length) {
        _resolve(store)
        return;
      }
      runner(curWorker)
    }
    curWorker.onerror = function (event) {
      console.dir('test error', event)
    }
  }

  for (let i = 0; i < max; i++) {
    let worker = threadPool[i];
    if (!worker) {
      worker = new Worker(url)
      threadPool[i] = worker;
    }
    runner(worker)
  }
  return p
}

/**
 * clear thread pool
 */
export const clearPool = () => {
  threadPool.forEach(worker => {
    worker.terminate()
  })
  threadPool = [];
}

export default pool;

线程池源码

运行结果 见上文截图。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant