2024-11-08 20:56:06 +08:00

255 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
sidebar_position: 3
description: "轻松地将项目逻辑多线程化。"
---
# 自定义多线程扩展
在之前,如果想为项目编写任何的多线程代码,因为 Worker 本身的易用性差,加上平台之间实现的差异,都会非常麻烦。
为此,社区版新增了自定义多线程扩展的支持,以简化多线程代码的编写,下面以计算斐波那契数列函数为例演示如何轻松编写多线程代码。
## 启用多线程扩展
依次点击编辑器的菜单项 **项目 - 社区版设置**,然后勾选 **项目多线程扩展** 即可。
## 创建多线程扩展
依次点击编辑器的菜单项 **扩展 - 创建新扩展插件... - 项目多线程扩展**
这会在项目内的 worker 目录中以默认模板创建一个多线程扩展。
![custom worker struct](./assets/custom_worker_struct.png)
- `src` 多线程源码目录
- `index.js` 入口脚本
- `math.js` 含有 `add` 加法函数的示范脚本
- `creator-worker.d.ts` 提供代码类型提示
- `jsconfig.json` JavaScript 语言服务器配置
## 初识多线程架构
在编写多线程代码时,你需要时刻清楚**多线程扩展内的脚本会在 Worker 线程中执行**,所以不能直接导入项目内的脚本文件。
以下为 `math.js` 的内容,虽然只有几句代码,但这就已经是一个多线程函数的完整实现!
```js
const { registerHandler } = require("ipc-worker.js");
export function add(x, y, callback) {
callback(x + y);
}
registerHandler("math", {
add,
});
```
你已经可以在项目中直接调用这个函数:
```js
worker.math.add([1, 2], ([v]) => {
console.log('Worker math add result:', v);
});
```
这个函数将在 Worker 线程内执行,而不会阻塞主线程。
让我们来编写一个在 Worker 线程中计算斐波那契数列的函数,来深入了解多线程代码的编写!
## 编写多线程脚本
### 创建脚本
我们先在 `src` 目录创建一个 `fibonacci.js` 脚本。
然后在 `index.js` 添加新的一行来导入它:
```js
require("fibonacci.js");
```
这样引擎在创建 Worker 时才会执行这个新脚本。
### 编写函数
`fibonacci.js` 脚本中实现计算斐波那契的函数:
```js
function _fibonacci(n) {
if (n <= 0) return 0;
if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
```
### 导出函数到主线程
现在,虽然在 Worker 线程中我们有了这个函数,但是我们无法在主线程调用它。
与 Worker 线程的通信通常使用 `postMessage``onMessage` 进行,但是需要处理很多边缘情况,而且这样的开发体验也较差,所以社区版提供了一个封装。
我们需要导入 `registerHandler` 函数:
```js
const { registerHandler } = require("ipc-worker.js");
```
该函数的签名是:
```ts
export function registerHandler(name: string, handler: object): void;
```
调用函数时,函数会执行以下操作:
- 在全局变量 `worker` 的对象上增加一个与传入 `name` 一样的对象属性。
- 遍历传入 `handler` 对象上的所有属性,按规则在 `worker.<name>` 对象上创建对应的函数。
也就是说,我们只需要将 `fibonacci` 函数传入到 `registerHandler` 函数并调用,函数就可以在主线程中调用了!
以下是完整的 `fibonacci.js` 内容:
```js
const { registerHandler } = require("ipc-worker.js");
function _fibonacci(n) {
if (n <= 0) return 0;
if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
function fibonacci(n, callback) {
callback(_fibonacci(n));
}
registerHandler("utils", {
fibonacci,
});
```
### 导出的内部原理
你可能注意到了我们导出的是另一个函数,而不是直接导出 `_fibonacci` 函数。
因为这是实现一个跨线程调用函数时需要遵循的规范:
-`postMessage` 的要求一样,函数的所有参数必须是可序列化的。
- 当函数被调用时,会在函数最后一个参数传入一个回调函数,当需要返回到主线程时,请调用该函数。
像上面 `fibonacci` 函数的实现,在调用 `_fibonacci` 拿到计算结果后,通过调用 `callback(v)` 将值返回到主线程。
而在主线程中,我们需要像这样从主线程调用 Worker 线程中的这个函数:
```js
worker.utils.fibonacci([10], ([v]) => {
console.log('Worker fibonacci result:', v);
});
```
你可能注意到了,第一个参数是数组,而第二个参数的回调的第一个参数也是数组,这也是规范。
为了提高跨线程通信的性能,减少垃圾回收的频率,所以选择了这种调用的方式。
你可以这样理解 `worker.xxx.xxx()` 的调用签名:
```
worker.utils.fibonacci(args, (values) => ...);
// utils: 要调用的 handler 名称
// fibonacci: 要调用 handler 中的 Worker 函数名称
// args: 传入到 Worker 函数的所有参数
// values: Worker 函数返回时的回调,参数是返回值数组
```
这很好理解,我们再举个多参数调用的例子:
```js
// Worker 线程的函数
function handle(a, b, c, callback) {
// a = "ye.", b = {}, c = 1000
callback(1, "text", { prop: 2 });
}
// 主线程的调用方式
worker.utils.handle(["ye.", {}, 1000], ([v1, v2, v3]) => {
// v1 = 1, v2 = "text", v3 = { prop: 2 }
});
```
### 更多导出场景
无参数函数的实现与调用:
```js
// Worker 线程的函数
function setValue(callback) {
// ...
callback();
}
// 主线程的调用方式
worker.utils.setValue(() => {
// ok.
});
```
无需返回的函数的实现与调用(这同时能节省 Worker 的通信开销,因为只需要单向通信!):
```js
// Worker 线程的函数
function setValue(v) {
// ...
// 执行完成之后不调用 callback甚至不用声明
}
// 主线程的调用方式
worker.utils.setValue(["ye."]);
```
无参数也无返回的函数的实现与调用(这同时能节省 Worker 的通信开销,因为只需要单向通信!):
```js
// Worker 线程的函数
function setValue() {
// ...
// 执行完成之后不调用 callback甚至不用声明
}
// 主线程的调用方式
worker.utils.setValue();
```
除了函数之外你还可以导出值、getter/setter 属性,但需要注意需通过 `get_xxx``set_xxx``write_xxx` 三个代理函数进行访问与修改:
```js
// Worker 线程中:
registerHandler("Date", {
time: 1,
});
// 主线程中:
// 获取值
worker.Date.get_time(([v]) => {
// v is 1.
});
// 修改值,不会回调,性能比 write 更高
worker.Date.set_time([100]);
// 修改值,会回调以通知操作已执行完毕
worker.Date.write_time([100], () => {
// finish.
});
```
## 编译多线程扩展
每次修改扩展代码之后,需要手动点击 **项目 - 重新编译多线程扩展** 以生效。
特别注意:**就像修改多线程的设置会影响到所有项目一样,多线程扩展的编译结果也是所有项目共用的!**
所以当你**构建某个项目之前,必须确保最后一次编译是当前项目的多线程扩展**