头图

🚀🚀workerpool,JavaScript强大的线程池库!

workerpool:Node.js和浏览器中的任务分发利器

原文链接:https://github.com/josdejong/workerpool
作者:Jos de Jong
译者:倔强青铜三

前言

大家好,我是倔强青铜三。是一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新,欢迎关注我,微信公众号:倔强青铜三。欢迎点赞、收藏、关注,一键三连!!!

workerpool是一个强大的库,它为Node.js和浏览器环境提供了一种简单的方式来创建任务分发池。通过这个库,你可以轻松地将计算密集型任务卸载到一个工作线程池中,从而避免阻塞主线程,提高应用程序的响应性和性能。无论你是前端开发者还是Node.js服务器端开发者,workerpool都能为你提供一个高效的解决方案来处理并发任务。

特性

  • 易于使用:简单几行代码即可上手。
  • 跨平台运行:既可在浏览器中运行,也可在Node.js环境中使用。
  • 动态卸载函数:可以将函数动态地卸载到工作线程中执行。
  • 通过代理访问工作线程:提供了一种自然的、基于Promise的代理方式来访问工作线程,就像它们直接在主应用程序中可用一样。
  • 取消运行中的任务:能够取消正在执行的任务。
  • 设置任务超时:可以为任务设置超时时间。
  • 处理崩溃的工作线程:能够妥善处理工作线程崩溃的情况。
  • 体积小巧:仅9 kB(minified and gzipped)。
  • 支持可转移对象:仅限于Web Workers和worker_threads。

为什么需要workerpool

JavaScript基于单个事件循环,一次只能处理一个事件。在Node.js中,虽然所有I/O代码都是非阻塞的,但所有非I/O代码却是阻塞的。这意味着CPU密集型任务会阻塞其他任务的执行。在浏览器环境中,执行CPU密集型任务时,浏览器不会响应用户事件(如鼠标点击),导致浏览器“卡住”。在Node.js服务器端,执行单个重量级请求时,服务器不会响应任何新请求。因此,为了提高前端进程的用户体验,CPU密集型任务应该从主事件循环中卸载到专门的工作线程上。在浏览器环境中可以使用Web Workers,在Node.js中则可以使用子进程和worker_threads。将应用程序拆分成独立的、解耦的部分,这些部分可以以并行化的方式独立运行,从而实现通过隔离进程和消息传递来达到并发的架构。

安装

通过npm安装:

npm install workerpool

加载

在Node.js应用程序中加载workerpool(无论是主应用程序还是工作线程):

const workerpool = require('workerpool');

在浏览器中加载workerpool

<script src="workerpool.js"></script>

在浏览器的Web Worker中加载workerpool

importScripts('workerpool.js');

在React或webpack5中设置workerpool需要额外的配置步骤,具体请参考webpack5部分的说明。

使用方法

动态卸载函数

以下示例中有一个add函数,它被动态地卸载到工作线程中执行给定的一组参数。

myApp.js

const workerpool = require('workerpool');
const pool = workerpool.pool();

function add(a, b) {
  return a + b;
}

pool
  .exec(add, [3, 4])
  .then(function (result) {
    console.log('result', result); // 输出7
  })
  .catch(function (err) {
    console.error(err);
  })
  .then(function () {
    pool.terminate(); // 完成后终止所有工作线程
  });

请注意,函数和参数都必须是静态的,并且可以被序列化,因为它们需要以序列化的形式发送到工作线程。对于大型函数或函数参数,将数据发送到工作线程的开销可能会很大。

专用工作线程

可以在单独的脚本中创建一个专用工作线程,然后通过工作线程池来使用它。

myWorker.js

const workerpool = require('workerpool');

// 故意低效的斐波那契数列实现
function fibonacci(n) {
  if (n < 2) return n;
  return fibonacci(n - 2) + fibonacci(n - 1);
}

// 创建工作线程并注册公共函数
workerpool.worker({
  fibonacci: fibonacci,
});

这个工作线程可以通过工作线程池来使用:

myApp.js

const workerpool = require('workerpool');

// 使用外部工作线程脚本创建工作线程池
const pool = workerpool.pool(__dirname + '/myWorker.js');

// 通过exec在工作线程上运行注册的函数
pool
  .exec('fibonacci', [10])
  .then(function (result) {
    console.log('Result: ' + result); // 输出55
  })
  .catch(function (err) {
    console.error(err);
  })
  .then(function () {
    pool.terminate(); // 完成后终止所有工作线程
  });

// 或者通过代理在工作线程上运行注册的函数:
pool
  .proxy()
  .then(function (worker) {
    return worker.fibonacci(10);
  })
  .then(function (result) {
    console.log('Result: ' + result); // 输出55
  })
  .catch(function (err) {
    console.error(err);
  })
  .then(function () {
    pool.terminate(); // 完成后终止所有工作线程
  });

工作线程也可以异步初始化:

myAsyncWorker.js

define(['workerpool/dist/workerpool'], function (workerpool) {
  // 故意低效的斐波那契数列实现
  function fibonacci(n) {
    if (n < 2) return n;
    return fibonacci(n - 2) + fibonacci(n - 1);
  }

  // 创建工作线程并注册公共函数
  workerpool.worker({
    fibonacci: fibonacci,
  });
});

示例

示例位于examples目录中:

https://github.com/josdejong/workerpool/tree/master/examples

API

workerpool的API包含两部分:一个用于创建工作线程池的函数workerpool.pool,以及一个用于创建工作线程的函数workerpool.worker

pool

使用函数workerpool.pool可以创建工作线程池:

workerpool.pool([script: string] [, options: Object]) : Pool

当提供script参数时,提供的脚本将作为专用工作线程启动。如果没有提供script参数,则会启动一个默认工作线程,可以通过Pool.exec动态卸载函数。在Node.js中,script必须是绝对文件路径,例如__dirname + '/myWorker.js'。在浏览器环境中,script也可以是data URL,例如'data:application/javascript;base64,...'。这允许将工作线程的打包代码嵌入到主应用程序中。请参见examples/embeddedWorker中的演示。

可用的选项如下:

  • minWorkers: number | 'max'。必须初始化并保持可用的最小工作线程数量。将其设置为'max'将创建maxWorkers默认工作线程(见下文)。
  • maxWorkers: number。默认的maxWorkers数量是CPU数量减一。如果无法确定CPU数量(例如在旧浏览器中),maxWorkers设置为3。
  • maxQueueSize: number。允许排队的任务的最大数量。可以用来防止内存耗尽。如果超过最大值,添加新任务将抛出错误。默认值为Infinity
  • workerType: 'auto' | 'web' | 'process' | 'thread'

    • 如果是'auto'(默认值),workerpool将自动选择合适类型的工作线程:在浏览器环境中使用'web'。在Node.js环境中,如果可用(Node.js >= 11.7.0),则使用worker_threads,否则使用child_process
    • 如果是'web',则使用Web Worker。仅在浏览器环境中可用。
    • 如果是'process',则使用child_process。仅在Node.js环境中可用。
    • 如果是'thread',则使用worker_threads。如果worker_threads不可用,则抛出错误。仅在Node.js环境中可用。
  • workerTerminateTimeout: number。终止工作线程时等待工作线程清理资源的超时时间(以毫秒为单位)。默认值为1000
  • abortListenerTimeout: number。等待中止监听器的超时时间(以毫秒为单位),超时后将强制停止并触发清理。默认值为1000
  • forkArgs: String[]。对于process工作线程类型。作为args传递给child_process.fork的数组。
  • forkOpts: Object。对于process工作线程类型。作为options传递给child_process.fork的对象。请参见Node.js文档以了解可用选项。
  • workerOpts: Object。对于web工作线程类型。传递给Web Worker构造函数的对象。请参见WorkerOptions规范以了解可用选项。
  • workerThreadOpts: Object。对于worker工作线程类型。传递给worker_threads.options的对象。请参见Node.js文档以了解可用选项。
  • onCreateWorker: Function。每当创建工作线程时调用的回调。它可以用来为每个工作线程分配资源,例如。回调的参数是一个具有以下属性的对象:

    • forkArgs: String[]:此池的forkArgs选项。
    • forkOpts: Object:此池的forkOpts选项。
    • workerOpts: Object:此池的workerOpts选项。
    • script: string:此池的script选项。
      可选地,此回调可以返回一个对象,包含上述一个或多个属性。提供的属性将用于覆盖正在创建工作线程的池属性。
  • onTerminateWorker: Function。每当终止工作线程时调用的回调。它可以用来释放可能为这个特定工作线程分配的资源。回调的参数是一个为onCreateWorker描述的对象,每个属性都设置为正在终止的工作线程的值。
  • emitStdStreams: boolean。对于processthread工作线程类型。如果为true,工作线程将发出stdoutstderr事件,而不是将其传递到父流。默认值为false
关于'workerType'的重要说明:当从和向工作线程发送和接收原始数据类型(纯JSON)时,不同的工作线程类型('web''process''thread')可以互换使用。但是,当使用更高级的数据类型(如缓冲区)时,API和返回结果可能会有所不同。在这种情况下,最好不要使用'auto'设置,而是有一个固定的'workerType',并有良好的单元测试。

工作线程池包含以下函数:

  • Pool.exec(method: Function | string, params: Array | null [, options: Object]) : Promise<any, Error>

执行工作线程上的函数,并给出参数。

  • method是字符串时,工作线程上必须存在具有此名称的方法,并且必须注册以使其可以通过池访问。该函数将在工作线程上执行,并给出参数。
  • method是函数时,提供的函数fn将被序列化,发送到工作线程,并在那里与提供的参数一起执行。提供的函数必须是静态的,它不能依赖于周围作用域中的变量。
  • 可用的选项如下:

    • on: (payload: any) => void。事件监听器,用于处理工作线程为此次执行发送的事件。请参见事件部分以了解更多信息。
    • transfer: Object[]。要发送到工作线程的可转移对象列表。process工作线程类型不支持此选项。请参见示例以了解用法。
  • Pool.proxy() : Promise<Object, Error>

创建工作线程池的代理。代理包含工作线程上所有方法的代理。所有方法返回的都是解析方法结果的Promise。

  • Pool.stats() : Object

获取工作线程、活跃任务和待处理任务的统计信息。

返回一个包含以下属性的对象:

{
    totalWorkers: 0,
    busyWorkers: 0,
    idleWorkers: 0,
    pendingTasks: 0,
    activeTasks: 0
}
  • Pool.terminate([force: boolean [, timeout: number]]) : Promise<void, Error>

如果参数forcefalse(默认值),工作线程将完成它们正在处理的任务,然后自行终止。任何待处理的任务将被拒绝,并抛出错误'Pool terminated'。当forcetrue时,所有工作线程将立即终止,而不完成正在运行的任务。如果提供了timeout,当超时到期且工作线程尚未完成时,将强制终止工作线程。

Pool.exec函数和代理函数都返回一个Promise。Promise具有以下函数可用:

  • Promise.then(fn: Function<result: any>) : Promise<any, Error>

获取Promise解析后的结果。

  • Promise.catch(fn: Function<error: Error>) : Promise<any, Error>

获取Promise拒绝时的错误。

  • Promise.finally(fn: Function<void>)

无论Promise是解析还是拒绝,都会执行的逻辑。

  • Promise.cancel() : Promise<any, Error>

可以取消正在运行的任务。执行任务的工作线程将被强制立即终止。Promise将被拒绝,并抛出Promise.CancellationError

  • Promise.timeout(delay: number) : Promise<any, Error>

如果任务在给定的延迟(以毫秒为单位)内未解析或拒绝,则取消正在运行的任务。计时器将在任务实际开始时启动,而不是在任务创建并排队时启动。执行任务的工作线程将被强制立即终止。Promise将被拒绝,并抛出Promise.TimeoutError

示例用法:

const workerpool = require('workerpool');

function add(a, b) {
  return a + b;
}

const pool1 = workerpool.pool();

// 将函数卸载到工作线程
pool1
  .exec(add, [2, 4])
  .then(function (result) {
    console.log(result); // 将输出6
  })
  .catch(function (err) {
    console.error(err);
  });

// 创建专用工作线程
const pool2 = workerpool.pool(__dirname + '/myWorker.js');

// 假设myWorker.js包含一个名为'fibonacci'的函数
pool2
  .exec('fibonacci', [10])
  .then(function (result) {
    console.log(result); // 将输出55
  })
  .catch(function (err) {
    console.error(err);
  });

// 向工作线程发送可转移对象
// 假设myWorker.js包含一个名为'sum'的函数
const toTransfer = new Uint8Array(2).map((_v, i) => i);
pool2
  .exec('sum', [toTransfer], { transfer: [toTransfer.buffer] })
  .then(function (result) {
    console.log(result); // 将输出3
  })
  .catch(function (err) {
    console.error(err);
  });

// 创建myWorker.js的代理
pool2
  .proxy()
  .then(function (myWorker) {
    return myWorker.fibonacci(10);
  })
  .then(function (result) {
    console.log(result); // 将输出55
  })
  .catch(function (err) {
    console.error(err);
  });

// 创建具有指定最大工作线程数量的池
const pool3 = workerpool.pool({ maxWorkers: 7 });

worker

工作线程的构造方式如下:

workerpool.worker([methods: Object<String, Function>] [, options: Object]) : void

methods参数是可选的,可以是一个对象,包含工作线程中可用的函数。注册的函数将通过工作线程池可用。

可用的选项如下:

  • onTerminate: ([code: number]) => Promise<void> | void。每当终止工作线程时调用的回调。它可以用来释放可能为这个特定工作线程分配的资源。与池的onTerminateWorker的区别是,此回调在工作线程上下文中运行,而onTerminateWorker在主线程上执行。

示例用法:

// 文件myWorker.js
const workerpool = require('workerpool');

function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

// 创建工作线程并注册函数
workerpool.worker({
  add: add,
  multiply: multiply,
});

工作线程中的函数可以通过返回Promise来处理异步结果:

// 文件myWorker.js
const workerpool = require('workerpool');

function timeout(delay) {
  return new Promise(function (resolve, reject) {
    setTimeout(resolve, delay);
  });
}

// 创建工作线程并注册函数
workerpool.worker({
  timeout: timeout,
});

可以使用Transfer辅助类将可转移对象发送回池:

// 文件myWorker.js
const workerpool = require('workerpool');

function array(size) {
  var array = new Uint8Array(size).map((_v, i) => i);
  return new workerpool.Transfer(array, [array.buffer]);
}

// 创建工作线程并注册函数
workerpool.worker({
  array: array,
});

事件

可以使用workerEmit函数从工作线程向池发送数据,而任务正在执行:

workerEmit(payload: any) : unknown

此函数仅在工作线程内部以及在任务执行期间有效。

示例:

// 文件myWorker.js
const workerpool = require('workerpool');

function eventExample(delay) {
  workerpool.workerEmit({
    status: 'in_progress',
  });

  workerpool.workerEmit({
    status: 'complete',
  });

  return true;
}

// 创建工作线程并注册函数
workerpool.worker({
  eventExample: eventExample,
});

要接收这些事件,可以使用池exec方法的on选项:

pool.exec('eventExample', [], {
  on: function (payload) {
    if (payload.status === 'in_progress') {
      console.log('In progress...');
    } else if (payload.status === 'complete') {
      console.log('Done!');
    }
  },
});

工作线程API

工作线程可以访问worker API,其中包含以下方法:

  • emit: (payload: unknown | Transfer): void
  • addAbortListener: (listener: () => Promise<void>): void

可以通过worker.addAbortListener注册中止监听器,工作线程终止可能是可恢复的。如果所有注册的监听器都已解决,则工作线程将不会被终止,允许在某些情况下重用工作线程。

注意:为了成功清理操作,工作线程实现应该是异步的。如果工作线程被阻塞,则工作线程将被杀死。

function asyncTimeout() {
  var me = this;
  return new Promise(function (resolve) {
    let timeout = setTimeout(() => {
        resolve();
    }, 5000);

    // 注册一个监听器,它将在上面的超时触发之前解决。
    me.worker.addAbortListener(async function () {
        clearTimeout(timeout);
        resolve();
    });
  });
}

// 创建工作线程并注册公共函数
workerpool.worker(
  {
    asyncTimeout: asyncTimeout,
  },
  {
    abortListenerTimeout: 1000
  }
);

也可以通过worker.emitworker API发出事件:

// 文件myWorker.js
const workerpool = require('workerpool');

function eventExample(delay) {
  this.worker.emit({
    status: "in_progress",
  });
  workerpool.workerEmit({
    status: 'complete',
  });

  return true;
}

// 创建工作线程并注册函数
workerpool.worker({
  eventExample: eventExample,
});

实用工具

提供了以下属性以方便使用:

  • platform:JavaScript平台。要么是_node_,要么是_browser_。
  • isMainThread:代码是否在主线程中运行(工作线程不是)。
  • cpus:可用的CPU/核心数量。

路线图

  • 实现并行处理函数:mapreduceforEachfiltersomeevery等。
  • 在不支持Web Workers的旧浏览器上实现优雅降级:回退到在主应用程序中处理任务。
  • 实现会话支持:能够由单个工作线程处理一系列相关任务,该工作线程可以为会话保持状态。

相关库

最后感谢阅读!欢迎关注我,微信公众号:倔强青铜三。欢迎点赞收藏关注,一键三连!!!

倔强青铜三
28 声望0 粉丝