1e41f4b71Sopenharmony_ci# ArrayBuffer序列化和转移
2e41f4b71Sopenharmony_ci
3e41f4b71Sopenharmony_ci## 简介
4e41f4b71Sopenharmony_ci
5e41f4b71Sopenharmony_ci在应用开发中,为了避免主线程阻塞,提高应用性能,需要将一些耗时操作放在子线程中执行。此时,子线程就需要访问主线程中的数据。ArkTS采用了基于[消息通信的Actor并发模型](../arkts-utils/multi-thread-concurrency-overview.md#actor模型),不需要开发者去面对锁带来的一系列复杂偶发问题,同时并发度也相对较高。由于Actor并发模型具有内存隔离的特性,所以跨线程传输[普通对象](../arkts-utils/normal-object.md)时是使用拷贝的方式进行传递,就会导致这样一种现象:同一份数据,在主线程和子线程内分别占用一份内存。在数据量较小时,对应用性能不会有明显的影响;但是如果数据量较大时(比如传递一个音频或者视频数据),就可能会因为占用内存较多,内存资源不足,影响其他任务的执行。
6e41f4b71Sopenharmony_ci
7e41f4b71Sopenharmony_ci除了普通对象,ArkTS还支持在线程间传递ArrayBuffer对象。这是一种[可转移对象](../arkts-utils/transferabled-object.md),传递时不需要进行拷贝,所以不会出现同一份数据占用两份内存的情况。本篇文章将通过示例代码,对比两种数据对象在线程间传递时的性能数据,同时给出优化建议,使开发者可以更好地实现线程间大数据的传输。
8e41f4b71Sopenharmony_ci
9e41f4b71Sopenharmony_ci关于多线程的使用和原理,可参考[OpenHarmony多线程能力场景化示例实践](multi_thread_capability.md),本文将不再详细讲述。
10e41f4b71Sopenharmony_ci
11e41f4b71Sopenharmony_ci## 场景示例
12e41f4b71Sopenharmony_ci
13e41f4b71Sopenharmony_ci在应用开发中,会遇到需要进行图片处理的场景(比如需要调整一张图片的亮度、饱和度、大小等),为了避免阻塞主线程,可以将图片传递到子线程中执行这些操作。下面将分别通过拷贝和转移的方式,将图片传递到子线程中,并对比两种方式的性能数据。
14e41f4b71Sopenharmony_ci
15e41f4b71Sopenharmony_ci### 使用拷贝方式传递
16e41f4b71Sopenharmony_ci
17e41f4b71Sopenharmony_ci在ArkTS中,TaskPool传递ArrayBuffer数据时,默认使用转移的方式,通过调用task.setTransferList([])接口,可以切换成拷贝的方式。
18e41f4b71Sopenharmony_ci
19e41f4b71Sopenharmony_ci首先,实现一个需要在Task中执行的用于调整饱和度的接口。
20e41f4b71Sopenharmony_ci
21e41f4b71Sopenharmony_ci```typescript
22e41f4b71Sopenharmony_ci// code/Performance/PerformanceLibrary/feature/ThreadDataTransfer/src/main/ets/utils/TreadUtil.ets
23e41f4b71Sopenharmony_ci@Concurrent
24e41f4b71Sopenharmony_cifunction adjustImageValue(arrayBuffer: ArrayBuffer, lastAdjustData: number, currentAdjustData: number): ArrayBuffer {
25e41f4b71Sopenharmony_ci  return execColorInfo(arrayBuffer, lastAdjustData, currentAdjustData);
26e41f4b71Sopenharmony_ci}
27e41f4b71Sopenharmony_ci```
28e41f4b71Sopenharmony_ci
29e41f4b71Sopenharmony_ci然后,通过拷贝的方式将图片数据传递到Task中,并在Task中将图片进行饱和度调整。
30e41f4b71Sopenharmony_ci
31e41f4b71Sopenharmony_ci```typescript
32e41f4b71Sopenharmony_ci// code/Performance/PerformanceLibrary/feature/ThreadDataTransfer/src/main/ets/utils/TreadUtil.ets
33e41f4b71Sopenharmony_ci// 创建Task,传入数据
34e41f4b71Sopenharmony_cifunction createImageTask(arrayBuffer: ArrayBuffer, lastAdjustData: number, currentAdjustData: number, isParamsByTransfer: boolean): taskpool.Task {
35e41f4b71Sopenharmony_ci  let task: taskpool.Task = new taskpool.Task(adjustImageValue, arrayBuffer, lastAdjustData, currentAdjustData);
36e41f4b71Sopenharmony_ci  if (!isParamsByTransfer) { // 是否使用转移方式
37e41f4b71Sopenharmony_ci    task.setTransferList([]);
38e41f4b71Sopenharmony_ci  }
39e41f4b71Sopenharmony_ci  return task;
40e41f4b71Sopenharmony_ci}
41e41f4b71Sopenharmony_ci......
42e41f4b71Sopenharmony_ci// 创建taskNum个Task
43e41f4b71Sopenharmony_cifor (let i: number = 0; i < taskNum; i++) {
44e41f4b71Sopenharmony_ci  let arrayBufferSlice: ArrayBuffer = arrayBuffer.slice(arrayBuffer.byteLength / taskNum * i, arrayBuffer.byteLength / taskNum * (i + 1));
45e41f4b71Sopenharmony_ci  // 使用拷贝方式传入ArrayBuffer,所以isParamsByTransfer是false
46e41f4b71Sopenharmony_ci  taskPoolGroup.addTask(createImageTask(arrayBufferSlice, lastAdjustData, currentAdjustData, isParamsByTransfer));
47e41f4b71Sopenharmony_ci}
48e41f4b71Sopenharmony_cilet start: number = new Date().getTime();
49e41f4b71Sopenharmony_ci// 执行Task
50e41f4b71Sopenharmony_citaskpool.execute(taskPoolGroup).then((data: ArrayBuffer[]) => {
51e41f4b71Sopenharmony_ci  if (callback !== undefined) {
52e41f4b71Sopenharmony_ci    let end : number = new Date().getTime();
53e41f4b71Sopenharmony_ci    AppStorage.set<String>('timeCost', util.format('%s s', ((end - start) / 60).toFixed(2).toString()));
54e41f4b71Sopenharmony_ci    callback(concatenateArrayBuffers(data));
55e41f4b71Sopenharmony_ci  }
56e41f4b71Sopenharmony_ci}).catch((e: BusinessError) => {
57e41f4b71Sopenharmony_ci  Logger.error(e.message);
58e41f4b71Sopenharmony_ci})
59e41f4b71Sopenharmony_ci......
60e41f4b71Sopenharmony_ci```
61e41f4b71Sopenharmony_ci
62e41f4b71Sopenharmony_ci最后,主线程接收到Task执行完毕后返回的ArrayBuffer数据,转换为PixelMap后在Image组件上显示。
63e41f4b71Sopenharmony_ci```typescript
64e41f4b71Sopenharmony_ci// code/Performance/PerformanceLibrary/feature/ThreadDataTransfer/src/main/ets/view/AdjustImageView.ets
65e41f4b71Sopenharmony_ci......
66e41f4b71Sopenharmony_ci// 将处理后的ArrayBuffer转换为PixelMap,并在Image组件上显示
67e41f4b71Sopenharmony_cipixelMapProcessByTaskPool(this.pixelMap, this.lastAdjustData, this.currentAdjustData, this.currentTaskNum, 
68e41f4b71Sopenharmony_ci  this.isParamsByTransfer, (data: ArrayBuffer) => {
69e41f4b71Sopenharmony_ci    if (this.pixelMap !== undefined) {
70e41f4b71Sopenharmony_ci    const newPixel: image.PixelMap = this.pixelMap;
71e41f4b71Sopenharmony_ci    newPixel.writeBufferToPixels(data).then(() => {
72e41f4b71Sopenharmony_ci      this.pixelMap = newPixel;
73e41f4b71Sopenharmony_ci      this.lastAdjustData = Math.round(value);
74e41f4b71Sopenharmony_ci      this.isPixelMapChanged = !this.isPixelMapChanged;
75e41f4b71Sopenharmony_ci      this.deviceListDialogController.close();
76e41f4b71Sopenharmony_ci      this.postState = true;
77e41f4b71Sopenharmony_ci    });
78e41f4b71Sopenharmony_ci  }
79e41f4b71Sopenharmony_ci});
80e41f4b71Sopenharmony_ci......
81e41f4b71Sopenharmony_ci```
82e41f4b71Sopenharmony_ci编译运行后,通过脚本工具抓取Trace并在SmartPerf Host中查看,如图1所示。其中,All Heap表示应用占用的内存,BeforePassParameter表示ArrayBuffer开始从主线程传递到子线程,AfterPassParameter表示子线程收到完整的ArrayBuffer数据。
83e41f4b71Sopenharmony_ci
84e41f4b71Sopenharmony_ci图1 拷贝方式Trace泳道图
85e41f4b71Sopenharmony_ci
86e41f4b71Sopenharmony_ci![image-20240702184652708](figures/thread_data_transfer_image_01.png)
87e41f4b71Sopenharmony_ci
88e41f4b71Sopenharmony_ci图1中可以看到,ArrayBuffer传递到TaskPool时,内存有2.5M的上升,耗时是19ms。
89e41f4b71Sopenharmony_ci
90e41f4b71Sopenharmony_ci### 使用转移方式传递
91e41f4b71Sopenharmony_ci
92e41f4b71Sopenharmony_ci在TaskPool中,传递ArrayBuffer数据,是默认使用转移方式的,所以在上面示例的基础上,去除task.setTransferList([])接口就可以实现。
93e41f4b71Sopenharmony_ci```typescript
94e41f4b71Sopenharmony_ci// code/Performance/PerformanceLibrary/feature/ThreadDataTransfer/src/main/ets/utils/TreadUtil.ets
95e41f4b71Sopenharmony_ci// 创建Task,传入数据
96e41f4b71Sopenharmony_cifunction createImageTask(arrayBuffer: ArrayBuffer, lastAdjustData: number, currentAdjustData: number, isParamsByTransfer: boolean): taskpool.Task {
97e41f4b71Sopenharmony_ci  let task: taskpool.Task = new taskpool.Task(adjustImageValue, arrayBuffer, lastAdjustData, currentAdjustData);
98e41f4b71Sopenharmony_ci  if (!isParamsByTransfer) { // 是否使用转移方式
99e41f4b71Sopenharmony_ci    task.setTransferList([]);
100e41f4b71Sopenharmony_ci  }
101e41f4b71Sopenharmony_ci  return task;
102e41f4b71Sopenharmony_ci}
103e41f4b71Sopenharmony_ci......
104e41f4b71Sopenharmony_ci// 创建taskNum个Task
105e41f4b71Sopenharmony_cifor (let i: number = 0; i < taskNum; i++) {
106e41f4b71Sopenharmony_ci  let arrayBufferSlice: ArrayBuffer = arrayBuffer.slice(arrayBuffer.byteLength / taskNum * i, arrayBuffer.byteLength / taskNum * (i + 1));
107e41f4b71Sopenharmony_ci  // 使用转移方式传入ArrayBuffer,所以isParamsByTransfer是true
108e41f4b71Sopenharmony_ci  taskPoolGroup.addTask(createImageTask(arrayBufferSlice, lastAdjustData, currentAdjustData, isParamsByTransfer));
109e41f4b71Sopenharmony_ci}
110e41f4b71Sopenharmony_cilet start: number = new Date().getTime();
111e41f4b71Sopenharmony_ci// 执行Task
112e41f4b71Sopenharmony_citaskpool.execute(taskPoolGroup).then((data: ArrayBuffer[]) => {
113e41f4b71Sopenharmony_ci  if (callback !== undefined) {
114e41f4b71Sopenharmony_ci    let end : number = new Date().getTime();
115e41f4b71Sopenharmony_ci    AppStorage.set<String>('timeCost', util.format('%s s', ((end - start) / 60).toFixed(2).toString()));
116e41f4b71Sopenharmony_ci    callback(concatenateArrayBuffers(data));
117e41f4b71Sopenharmony_ci  }
118e41f4b71Sopenharmony_ci}).catch((e: BusinessError) => {
119e41f4b71Sopenharmony_ci  Logger.error(e.message);
120e41f4b71Sopenharmony_ci})
121e41f4b71Sopenharmony_ci......
122e41f4b71Sopenharmony_ci```
123e41f4b71Sopenharmony_ci
124e41f4b71Sopenharmony_ci编译运行后,通过脚本工具抓取Trace并在SmartPerf Host中查看,如图2所示。
125e41f4b71Sopenharmony_ci
126e41f4b71Sopenharmony_ci图2 转移方式Trace泳道图
127e41f4b71Sopenharmony_ci
128e41f4b71Sopenharmony_ci![img](figures/thread_data_transfer_image_02.jpg)
129e41f4b71Sopenharmony_ci
130e41f4b71Sopenharmony_ci在图2中可以看到,ArrayBuffer传递到TaskPool时,内存并没有明显的变化,耗时只有5.2ms。
131e41f4b71Sopenharmony_ci
132e41f4b71Sopenharmony_ci### 性能对比
133e41f4b71Sopenharmony_ci
134e41f4b71Sopenharmony_ci通过对比上面两个场景中的Trace数据可以发现,相对于拷贝,转移的内存占用明显变少,耗时也减少了72%。
135e41f4b71Sopenharmony_ci
136e41f4b71Sopenharmony_ci使用拷贝方式传递数据时,会将ArrayBuffer对象拷贝一次。不仅会多占用了一部分内存,还会消耗一定的时间进行拷贝对象的序列化操作。而通过转移的方式传递数据时,并不需要将传递的对象拷贝一次,而是通过地址转移进行序列化,将ArrayBuffer的内存资源从原始的缓冲区分离出来,附加到子线程TaskPool创建的缓冲区对象中,也就是将ArrayBuffer的所有权从主线程移交给了子线程。
137e41f4b71Sopenharmony_ci
138e41f4b71Sopenharmony_ci所以,使用转移方式,可以减少线程间传递数据时的内存占用和CPU耗时。
139e41f4b71Sopenharmony_ci
140e41f4b71Sopenharmony_ci## 使用建议
141e41f4b71Sopenharmony_ci
142e41f4b71Sopenharmony_ci在ArkTS的多线程中传递数据时,应保证线程间的通信数据量尽可能的小,输入输出都要简单,避免传输影响耗时;而且并发任务要相对独立,不要频繁跨线程交互。上一部分的例子中,虽然将图片处理的操作放在了TaskPool子线程中,但是由于图片数据较大,处理时间还是较长,如图3所示。其中,BeforePassParameter表示ArrayBuffer开始从主线程传递到子线程,AfterPassParameter表示子线程收到完整的ArrayBuffer数据,AfterImageDarken表示计算(图片饱和度调节)结束。在图中可以看到,有5.8s的耗时,虽然在这段时间内并不会阻塞主线程中的操作,但是对于用户来说,等待时间依然较长。
143e41f4b71Sopenharmony_ci
144e41f4b71Sopenharmony_ci图3 子线程计算耗时泳道图
145e41f4b71Sopenharmony_ci
146e41f4b71Sopenharmony_ci![img](figures/thread_data_transfer_image_03.jpg)
147e41f4b71Sopenharmony_ci
148e41f4b71Sopenharmony_ci下面将通过示例代码说明如何进一步优化图片处理的时间。详细代码请参考[ThreadDataTransfer](https://gitee.com/openharmony/applications_app_samples/tree/master/code/Performance/PerformanceLibrary/feature/ThreadDataTransfer)。
149e41f4b71Sopenharmony_ci```typescript
150e41f4b71Sopenharmony_ci// code/Performance/PerformanceLibrary/feature/ThreadDataTransfer/src/main/ets/utils/TreadUtil.ets
151e41f4b71Sopenharmony_ci......
152e41f4b71Sopenharmony_ci// 根据传入的taskNum,创建对应数量的Task
153e41f4b71Sopenharmony_ciexport async function pixelMapProcessByTaskPool(pixelMap: image.PixelMap, lastAdjustData: number, currentAdjustData: number, taskNum: number, isParamsByTransfer: boolean, callback?: Callback<ArrayBuffer>): Promise<void> {
154e41f4b71Sopenharmony_ci  let arrayBuffer: ArrayBuffer = await convertPixelMapToArrayBuffer(pixelMap);
155e41f4b71Sopenharmony_ci  let taskPoolGroup: taskpool.TaskGroup = new taskpool.TaskGroup();
156e41f4b71Sopenharmony_ci  for (let i: number = 0; i < taskNum; i++) {
157e41f4b71Sopenharmony_ci    let arrayBufferSlice: ArrayBuffer = arrayBuffer.slice(arrayBuffer.byteLength / taskNum * i, arrayBuffer.byteLength / taskNum * (i + 1));
158e41f4b71Sopenharmony_ci    taskPoolGroup.addTask(createImageTask(arrayBufferSlice, lastAdjustData, currentAdjustData, isParamsByTransfer));
159e41f4b71Sopenharmony_ci  }
160e41f4b71Sopenharmony_ci  let start: number = new Date().getTime();
161e41f4b71Sopenharmony_ci  taskpool.execute(taskPoolGroup).then((data: ArrayBuffer[]) => {
162e41f4b71Sopenharmony_ci    if (callback !== undefined) {
163e41f4b71Sopenharmony_ci      let end : number = new Date().getTime();
164e41f4b71Sopenharmony_ci      AppStorage.set<String>('timeCost', util.format('%s s', ((end - start) / 60).toFixed(2).toString()));
165e41f4b71Sopenharmony_ci      // 将Task处理完成的数据合并在一个ArrayBuffer中
166e41f4b71Sopenharmony_ci      callback(concatenateArrayBuffers(data));
167e41f4b71Sopenharmony_ci    }
168e41f4b71Sopenharmony_ci  }).catch((e: BusinessError) => {
169e41f4b71Sopenharmony_ci    Logger.error(e.message);
170e41f4b71Sopenharmony_ci  })
171e41f4b71Sopenharmony_ci}
172e41f4b71Sopenharmony_ci```
173e41f4b71Sopenharmony_ci在这段代码中,将图片转换为ArrayBuffer后,并没有直接传递到子线程中,而是先进行了切片处理,分成taskNum个独立的ArrayBuffer。之后分别传入到taskNum个Task中,并通过TaskGroup统一管理。在图片饱和度调节完成后,再将taskNum个处理结果拼接成一个ArrayBuffer,并转换为PixelMap在Image组件上显示。此示例代码也是使用了转移方式进行传递,因为上一部分的示例已经说明了转移比拷贝的方式更加高效,所以这里不再进行重复的对比。编译运行代码后,通过脚本工具抓取Trace并在SmartPerf Host中查看,如图4所示。
174e41f4b71Sopenharmony_ci
175e41f4b71Sopenharmony_ci图4 图片切成3份处理后的泳道图
176e41f4b71Sopenharmony_ci
177e41f4b71Sopenharmony_ci![image-20240702185135759](figures/thread_data_transfer_image_04.png)
178e41f4b71Sopenharmony_ci
179e41f4b71Sopenharmony_ci通过图4可以看到,将原ArrayBuffer切成3段,分别放在3个子线程中处理,图片饱和度调节的操作耗时只有3.4s,计算时间减少了44%。因为有3个线程在同时进行操作,大大减少了计算的耗时。但是,这并不代表可以随意增加Task的数量。由于在TaskPool中,并不是创建了多少个Task,就会有多少个Task在同时执行。随着Task的增多,同时执行的线程也会越多,并发执行的任务就会增多,CPU的占用率就会升高,从而影响到其他需要CPU计算的任务。虽然在硬件配置高的设备上表现不明显,但是对于低配置的设备,影响会较大。下面的表格,列出了示例中不同数量Task时的CPU耗时供开发者参考。
180e41f4b71Sopenharmony_ci
181e41f4b71Sopenharmony_ci| **任务数** | **实际子线程数** | **序列化时间(ms)** | **计算时间(s)** |
182e41f4b71Sopenharmony_ci| ---------- | ---------------- | -------------------- | ----------------- |
183e41f4b71Sopenharmony_ci| 1          | 1                | 3.30792              | 3.967             |
184e41f4b71Sopenharmony_ci| 2          | 2                | 2.62345              | 2.646             |
185e41f4b71Sopenharmony_ci| 3          | 3                | 6.11258              | 1.875             |
186e41f4b71Sopenharmony_ci| 4          | 3                | 7.25295              | 2.579             |
187e41f4b71Sopenharmony_ci| 5          | 3                | 2.7032               | 2.266             |
188e41f4b71Sopenharmony_ci| 6          | 3                | 3.213                | 1.970             |
189e41f4b71Sopenharmony_ci| 7          | 3                | 5.260                | 2.096             |
190e41f4b71Sopenharmony_ci| 8          | 3                | 2.704                | 2.067             |
191e41f4b71Sopenharmony_ci| 9          | 3                | 3.986                | 2.116             |
192e41f4b71Sopenharmony_ci| 10         | 5                | 3.811                | 1.844             |
193e41f4b71Sopenharmony_ci| 15         | 5                | 2.879                | 2.079             |
194e41f4b71Sopenharmony_ci| 20         | 5                | 3.526                | 2.015             |
195e41f4b71Sopenharmony_ci
196e41f4b71Sopenharmony_ci因此,在分段传输大数据时,需要合理调整子线程的数量和传入子线程的数据量,加快子线程处理速度,同时减少对其他功能的影响,提升用户的应用体验。
197e41f4b71Sopenharmony_ci
198e41f4b71Sopenharmony_ci## 相关示例
199e41f4b71Sopenharmony_ci
200e41f4b71Sopenharmony_ci[ArrayBuffer序列化和转移示例代码](https://gitee.com/openharmony/applications_app_samples/tree/master/code/Performance/PerformanceLibrary/feature/ThreadDataTransfer)