Skip to main content

Web Workers

Web Workers 提供了一种在后台线程中运行脚本的方法,允许 web 应用在不干扰用户界面的情况下执行任务。它们特别适用于执行计算密集型任务或需要大量处理时间的任务。

通常,与 worker 一起工作需要大量的样板代码、postMessage 调用和事件监听器。Cornerstone 提供了一个简单的 API 来创建和使用 worker,隐藏了所有复杂性。

要求

你需要将 comlink 安装为应用程序的依赖项,仅此而已。
comlink 是一个库,允许你像使用本地对象一样使用 Web Workers,无需担心底层的消息传递。
尽管它不处理优先级队列、负载平衡或 worker 生命周期,但它提供了一个简单的 API 来与 worker 通信,Cornerstone 使用它来创建一个更强大且用户友好的 API。

使用示例

我们通过一个示例来解释 WebWorker API。假设你有一组希望在后台运行的函数。你需要编写一个通过 comlink 暴露这些函数的对象。

// file/location/my-awesome-worker.js

import { expose } from 'comlink';

const obj = {
counter: 69,
inc() {
obj.counter++;
console.debug('inc', obj.counter);
},
fib({ value }) {
if (value <= 1) {
return 1;
}
return obj.fib({ value: value - 1 }) + obj.fib({ value: value - 2 });
},
};

expose(obj);
note

如上所示,我们的对象可以包含任意数量的函数,并且可以持有本地状态。对于这些函数的唯一要求是,参数必须是可序列化的。这意味着不能将 DOM 元素、函数或其他非可序列化对象作为参数传递。

我们使用对象作为参数。因此,在上面的例子中,我们使用 fib({value}) 而不是 fib(value)value 只是参数名称,你可以为参数使用任何名称)。

现在,关键是要通知 Cornerstone 这个函数,以便它可以顺利在后台运行。让我们深入了解一下。

WebWorker 管理器

WebWorkerManager 在 WebWorker API 中起着至关重要的作用。它的主要功能是创建和监督 workers。
通过为任务分配不同的优先级和队列类型,你可以依赖该管理器根据指定的优先级有效地在后台执行任务。
此外,它处理 worker 的生命周期,分配工作负载,并提供一个用户友好的 API 来执行任务。

registerWorker

注册一个新的 worker 类型,使用唯一的名称和一个函数来告诉管理器有关 worker 的信息。

参数包括:

  • workerName:worker 类型的名称(应该是唯一的),我们稍后会用这个名称来调用函数。
  • workerFn:一个返回新 Worker 实例的函数(稍后会详细说明)。
  • options:一个对象,具有以下属性:
    • maxWorkerInstances(默认值为 1):可以创建的该类型 worker 的最大实例数。多个实例意味着如果对同一函数有多个调用,它们可以被分配到 worker 类型的其他实例上。
    • overwrite(默认值为 false):是否覆盖已注册的现有 worker 类型。
    • autoTerminateOnIdle(默认值为 false):用于在一定时间的空闲后终止 worker(以毫秒为单位)。这对于不常使用的 worker 非常有用,你希望在一段时间后终止它们。该方法的参数是 {enabled: boolean, idleTimeThreshold: number(ms)}
tip

请注意,如果 worker 被终止,并不意味着它被从管理器中销毁。实际上,任何后续对该 worker 的调用都会创建一个新的 worker 实例,一切会按预期工作。

因此,要注册我们上面创建的 worker,我们可以这样做:

import { getWebWorkerManager } from '@cornerstonejs/core';

const workerFn = () => {
return new Worker(
new URL(
'../relativePath/file/location/my-awesome-worker.js',
import.meta.url
),
{
name: 'ohif', // 浏览器用于在调试器中显示 worker 名称
}
);
};

const workerManager = getWebWorkerManager();

const options = {
// maxWorkerInstances: 1,
// overwrite: false
};

workerManager.registerWorker('ohif-worker', workerFn, options);

如上所示,你需要创建一个返回新 Worker 实例的函数。
为了使 worker 能正常工作,它应该位于主线程可以访问的目录(它可以是相对于当前目录的路径)。

note

你可以指定两个名称:

  1. workerFn 中的 name,它被浏览器用来在调试器中显示 worker 名称。
  2. 注册名称,我们稍后用它来调用函数。

executeTask

到目前为止,管理器只知道可用的 workers,但它不知道该如何使用它们。

executeTask 用于在 worker 上执行任务。它接受以下参数:

  • workerName:我们之前注册的 worker 类型的名称。
  • methodName:我们想在 worker 上执行的方法的名称(在上面的示例中是 fibinc)。
  • args(默认值 = {}):传递给函数的参数。参数必须是可序列化的,这意味着你不能传递 DOM 元素、函数或任何其他非可序列化对象(下面会说明如何传递非可序列化函数)。
  • options:一个对象,具有以下属性:
    • requestType(默认值 = RequestType.COMPUTE):请求的组别。用于优先级排序。默认值为 RequestType.COMPUTE,这是最低优先级。其他组别按优先级顺序为:RequestType.INTERACTIONRequestType.THUMBNAILRequestType.PREFETCH
    • priority(默认值 = 0):请求在指定组中的优先级。数字越小,优先级越高。
    • options(默认值 = {}):传递给池管理器的选项(通常不需要修改)。
    • callbacks(默认值 = []):传递任何你希望在 worker 中调用的函数。

现在,要在 worker 上执行 fib 函数,我们可以这样做:

import { getWebWorkerManager } from '@cornerstonejs/core';

const workerManager = getWebWorkerManager();
workerManager.executeTask('ohif-worker', 'fib', { value: 10 });

上述代码将在名称为 ohif-worker 的 worker 上执行 fib 函数,并传入参数 {value: 10}。当然,这是一个简化的示例,通常你需要在任务完成或失败时执行一些操作。由于 executeTask 返回的是一个 promise,你可以使用 thencatch 方法来处理结果。

workerManager
.executeTask('ohif-worker', 'fib', { value: 10 })
.then((result) => {
console.log('result', result);
})
.catch((error) => {
console.error('error', error);
});

或者你也可以直接等待结果:

try {
const result = await workerManager.executeTask('ohif-worker', 'fib', {
value: 10,
});
console.log('result', result);
} catch (error) {
console.error('error', error);
}

eventListeners

有时,您可能需要为 worker 提供回调函数。例如,如果你希望在 worker 进行进度更新时更新用户界面。正如前面提到的,不能直接将函数作为参数传递给 worker。但是,您可以通过使用 callbacks 属性来克服这个问题。这些 callbacks 会根据它们的位置作为参数传递给函数。

来自代码库的实际示例:

const results = await workerManager.executeTask(
'polySeg',
'convertContourToSurface',
{
polylines,
numPointsArray,
},
{
callbacks: [
(progress) => {
console.debug('progress', progress);
},
],
}
);

如你所见,我们将一个函数作为回调传递给 worker。这个函数作为下一个参数传递给 worker。

在 worker 中,我们有:

import { expose } from 'comlink';

const obj = {
async convertContourToSurface(args, ...callbacks) {
const { polylines, numPointsArray } = args;
const [progressCallback] = callbacks;
await this.initializePolySeg(progressCallback);
const results = await this.polySeg.instance.convertContourRoiToSurface(
polylines,
numPointsArray
);

return results;
},
};

expose(obj);

terminate

要终止 worker,可以使用 webWorkerManager.terminate(workerName)。它会停止所有给定 worker 的实例并清理资源。