鸿蒙5.0开发【应用并发设计(并发任务管理)】架构
目前已提供任务的不同执行方式,可以管理任务的执行顺序、优先级等,此章节对需要控制任务执行方式的场景进行分类,并分别介绍各类任务执行场景的HarmonyOS APP开发方案设计。
并发任务管理
概述
目前已提供任务的不同执行方式,可以管理任务的执行顺序、优先级等,此章节对需要控制任务执行方式的场景进行分类,并分别介绍各类任务执行场景的HarmonyOS APP开发方案设计。
多任务关联执行(串行顺序依赖)
-
场景描述
在应用业务实现过程中,可以使用串行队列机制,使多个任务按照一定的顺序依次执行,而不会出现并发或乱序的情况。一般情况下,串行队列可用于保证任务执行顺序与数据的一致性,避免多线程竞争和死锁问题,也可以简化多线程编程,适用于后置任务对前置任务存在依赖等场景。
常见的业务场景如下所示:
常见业务场景 具体业务描述 API执行队列 调用模块接口,存在执行顺序要求 渲染指令队列 操作DOM树、渲染等,有时序要求 启动时遍历程序包 启动遍历小程序包、清理包、资源加载等串行操作 -
实现方案介绍
ArkTS提供串行队列(SequenceRunner)能力,可以将多个任务加入到串行队列中,使加入队列的任务按顺序执行,也可以创建多组串行队列分组管理,以满足上述场景对串行执行的要求,开发者可通过以下步骤完成串行任务队列的创建与执行。实施方案介绍:
步骤一:创建需要串行执行的任务task_1 ~ task_n;
步骤二:创建串行队列runner;
步骤三:按照需要执行的顺序,依次将任务添加至runner内。
-
业务实现中的关键点
-
添加到串行队列的任务,不支持添加依赖addDependency;
额外添加的任务依赖可能导致串行队列冲突,即使添加的依赖本身遵循串行队列顺序也会被拦截。
-
添加到串行队列的任务,同样也受TaskPool执行任务的约束与限制;
当串行队列任务中任务执行失败、或被cancel,后续任务依旧会被执行。
-
-
案例参考
import { taskpool } from '@kit.ArkTS';
@Concurrent
function additionDelay(delay: number): void {
let start: number = new Date().getTime();
while (new Date().getTime() - start < delay) {
continue;
}
}
@Concurrent
function waitForRunner(resString: string): string {
return resString;
}
async function seqRunner() {
let result: string = "";
let task1: taskpool.Task = new taskpool.Task(additionDelay, 300);
let task2: taskpool.Task = new taskpool.Task(additionDelay, 200);
let task3: taskpool.Task = new taskpool.Task(additionDelay, 100);
let task4: taskpool.Task = new taskpool.Task(waitForRunner, 50);
let runner: taskpool.SequenceRunner = new taskpool.SequenceRunner();
runner.execute(task1).then(() => {
result += 'a';
});
runner.execute(task2).then(() => {
result += 'b';
});
runner.execute(task3).then(() => {
result += 'c';
});
await runner.execute(task4);
console.info("seqrunner: result is " + result);
}
-
与业界方案特殊差异说明
对于串行队列中某个任务执行失败后处理,业界尚无统一规范。
当前HarmonyOS APP开发中实现方式为继续后续任务的执行,若后续任务依赖上一个任务的结果输出,开发者需考虑任务失败场景的异常处理。
多任务关联执行(树状依赖)
-
场景描述
任务依赖是一种用于管理并发任务执行顺序的管理机制。通过任务依赖,可以指定一个任务在另一个任务完成后才能执行,从而构建出复杂的任务执行流程。任务依赖可以帮助开发者控制任务之间的依赖关系,确保任务按照预期的顺序执行。在TaskPool中,任务依赖是通过使用addDependency和removeDependency实现的。
常见的业务场景如下所示:
常见业务场景 具体业务描述 场景类型 CPU密集型 I/O密集型 同步任务 图片解码 解析一张大图,将大图数据拆成n份并放到n个任务中执行,执行完后通过这n个任务都依赖的一个任务对结果进行处理并返回 √ √ × 数据库操作 A任务执行需要B任务执行结果。B任务执行完将结果更新到数据库,再执行依赖B的A任务,A任务从数据库中获取B任务结果 × √ × 网络下载 A任务下载数据,B任务对数据进行处理。B任务执行依赖A任务结果 × √ × -
实现方案介绍
TaskPool目前提供addDependency(增加对其他任务的依赖)和removeDependency(移除对其他任务的依赖)两个接口,开发者可以通过调用这两个接口对任务设置依赖关系。任务默认不存在依赖关系,即不依赖其他任务和其他任务不依赖当前任务。
TaskPool内部维护一个任务依赖关系列表,调用addDependency/removeDependency对该列表进行数据更新。任务执行时前查询该列表,若该任务依赖其他任务,则该任务等待这些任务全部执行结束再执行;若该任务被其他任务依赖,则该任务执行结束会将依赖它的这些任务加入到Taskpool等待执行的队列中。
-
业务实现中的关键点
- 合理设置任务依赖关系。两个任务之间的执行不依赖对方的结果,则这两个任务无需设置依赖关系。
- 设置依赖关系需要考虑任务的优先级分配。避免高优先级任务依赖低优先级任务,造成高优先级设置失效。
- 任务依赖与任务组、串行队列的交互表现。已经执行过的任务不能设置依赖关系,任务组任务不能设置依赖关系,串行队列任务不能设置依赖关系,有依赖关系的任务执行结束后不能再次执行,有依赖关系的任务不能放入任务组,有依赖关系的任务不能放入串行队列。
-
案例参考
import { taskpool } from '@kit.ArkTS';
@Concurrent
function updateSAB(args: Uint32Array) {
if (args[0] == 0) {
args[0] = 100;
return 100;
} else if (args[0] == 100) {
args[0] = 200;
return 200;
} else if (args[0] == 200) {
args[0] = 300;
return 300;
}
return 0;
}
let sab = new SharedArrayBuffer(20);
let typedArray = new Uint32Array(sab);
let task1 = new taskpool.Task(updateSAB, typedArray);
let task2 = new taskpool.Task(updateSAB, typedArray);
let task3 = new taskpool.Task(updateSAB, typedArray);
task1.addDependency(task2);
task2.addDependency(task3);
taskpool.execute(task1).then((res: object) => {
console.info("taskpool:: execute task1 res: " + res);
})
taskpool.execute(task2).then((res: object) => {
console.info("taskpool:: execute task2 res: " + res);
})
taskpool.execute(task3).then((res: object) => {
console.info("taskpool:: execute task3 res: " + res);
})
-
与业界方案特殊差异说明
业界实现的多数任务依赖机制,与TaskPool提供的任务依赖机制表现无明显差异。
多任务同步等待结果(任务组)
-
场景描述
复数个任务并发执行,等所有任务执行完毕后统一返回一个完整结果,其中任意一个任务失败或取消会导致整个任务的结果失败。
常见业务场景 具体业务描述 场景类型 图片解析生成直方图 一张图片,为了并发加速,拆分成多个ArrayBuffer进行解析,在所有任务解析完成后统一返回结果将解析结果拼成一个完整的直方图进行渲染 CPU密集型 -
实现方案介绍
任务组能力目前通过TaskPool模块提供,以图片生成直方图为例进行介绍。
步骤一:定义并发函数(@Concurrent function),将承载图片数据的ArrayBuffer的解析逻辑封装在一个并发函数中;
步骤二:遍历ArrayBuffer,每个ArrayBuffer对应构造一个并发解析任务,将这些任务都添加到任务组中;
步骤三:通过TaskPool执行任务组,并在回调函数中执行直方图的拼接逻辑或异常处理逻辑。
-
业务实现中的关键点
- 任务组中的任务应是为了达成统一的目的,所有关联任务会输出一个统一的结果。
- 任务组的结果会等待所有任务执行结束后统一返回,所以如果需要一组任务中先执行完的任务优先处理的场景不要使用任务组。
-
案例参考
import { taskpool } from '@kit.ArkTS';
// 定义异步任务
@Concurrent
function imageProcessing(arrayBuffer: ArrayBuffer): ArrayBuffer {
// 此处添加业务逻辑,输入为ArrayBuffer,输出为存储了解析结果的ArrayBuffer
let message: ArrayBuffer = arrayBuffer;
return message;
}
let taskGroup: taskpool.TaskGroup = new taskpool.TaskGroup();
let TASK_POOL_CAPACITY: number = 10;
function histogramStatistic(pixelBuffer: ArrayBuffer): void {
// 往任务组中添加任务
let byteLengthOfTask: number = pixelBuffer.byteLength;
for (let i = 0; i < TASK_POOL_CAPACITY; i++) {
let dataSlice: Object = (i === TASK_POOL_CAPACITY - 1) ? pixelBuffer.slice(i * byteLengthOfTask) : pixelBuffer.slice(i * byteLengthOfTask, (i + 1) * byteLengthOfTask);
let task: taskpool.Task = new taskpool.Task(imageProcessing, dataSlice);
taskGroup.addTask(task);
}
taskpool.execute(taskGroup, taskpool.Priority.HIGH).then((res: Object[]): void | Promise<void> => {
// 结果数据处理
}).catch((error: Error) => {
console.error(`taskpool excute error: ${error}`);
})
}
多任务优先级调度
-
场景描述
优先级体现了任务对于应用当前业务场景的重要性。在并发场景下,系统和线程池的资源是有限的。在资源既定的情况下,系统会分配更多资源优先处理高优先级的任务,尽量保证此类任务的即时性,而低优先级的任务的调度则会相对滞后。TaskPool提供了多任务优先级调度机制,供开发者根据业务场景,合理设置优先级。
常见的业务场景如下所示:
常见业务场景 具体业务描述 场景类型 CPU密集型 I/O密集型 即时性 处理耗时的图片数据 拍摄输入或美化图片时会将图片数据放在TaskPool中处理,且需要在一定毫秒内将数据返回主线程渲染,为保证任务的即时性,影响用户体验,可以设置高优先级使任务被优先调度 √ × √ 日志落盘 将业务日志信息写到文件或数据库中,优先级较低 × √ × -
实现方案介绍
TaskPool提供了4种优先级属性: HIGH、MEDIUM、LOW、以及IDLE(高、中、低、后台)。
目前,仅有taskpool.Task支持优先级属性的设置(function类型不支持),默认优先级为MEDIUM。开发者可以通过taskpool.execute()接口在抛任务时显式指定优先级。
TaskPool底层对HIGH、MEDIUM、LOW任务的调度按照M:N:1进行,即每调用M个高优先级任务后会去调用1个中优先级任务。每调用N个中优先级任务后会去调用1个低优先级任务。通过配置比例关系,在保证高优先级任务优先执行的情况下,中优先级任务得到合理调度,低优先级任务不会饿死(目前M:N:1为5:5:1)。
优先级机制底层对接了QoS(quality-of-service),因此3种属性也对应着不同的线程优先级。高优先级的任务除了在TaskPool队列中会得到优先调度外,在CPU调度上也会获得更多的系统资源。
[Priority]的IDLE优先级是用来标记需要在后台运行的耗时任务(例如数据同步、备份。),它的优先级别是最低的。这种优先级标记的任务只会在所有线程都空闲的情况下触发执行,并且只会占用一个线程来执行。
-
业务实现中的关键点
- 合理设置高优先级的数量。某些场景下若有大量的高优先级任务,任务池将无法区分优先级差异,因此优先级调度可能退化成按入队顺序依次执行。此外,高优先级任务将会抢占系统资源,影响其他线程和应用的执行。
- 依赖多个任务的执行时需要考虑优先级的分配。避免高优先级任务依赖低优先级任务的执行,造成优先级倒置。
-
案例参考
import { taskpool } from '@kit.ArkTS';
function exec(bufferArray: ArrayBuffer): void {
let task = execColorInfo(bufferArray);
taskpool.execute(execColorInfo, taskpool.Priority.HIGH);
}
@Concurrent
async function execColorInfo(bufferArray: ArrayBuffer): Promise<ArrayBuffer> {
if (!bufferArray) {
return new ArrayBuffer(0);
}
const newBufferArr = bufferArray;
let colorInfo = new Uint8Array(newBufferArr);
let PIXEL_STEP = 2;
for (let i = 0; i < colorInfo?.length; i += PIXEL_STEP) {
// 数据处理
}
return newBufferArr;
}
-
与业界方案特殊差异说明
业界大多提供了优先级机制,与TaskPool中的优先级无明显差异。
-
不推荐应用实现方式
不推荐应用过多设置高优先级或者不合理的优先级。
任务延时调度
-
场景描述
在应用业务实现过程中,不是所有任务都需立刻执行,有些任务需延时一段时间后才需执行。
常见的业务场景如下所示:
常见业务场景 具体业务描述 缓存业务延时执行,不影响冷启动耗时 应用启动时,存在大量低优先级任务,例如二级界面的资源下载等,需要设置在3秒后执行,防止影响冷启动耗时 -
实现方案介绍
TaskPool提供了延时执行的能力,目前,只有taskpool.Task支持延时执行,开发者只需要如下三个步骤即可完成延时实现。实现方案介绍:
步骤一:创建Task对象;
步骤二:调用taskpool.executeDelayed实现延时执行,并在参数中依次填写延时时间:delayTime,执行任务:task,任务优先级(不填默认MEDIUM):priority;
步骤三:接收延时任务返回的数据并作处理。
-
业务实现中的关键点
- 非必需不建议使用任务延时调度。某些业务复杂的场景下使用任务延时调度可能会存在结果处理时序问题,从而导致应用业务出现问题。
- 不建议将多个任务延时到同一时间执行。这样会存在任务排队情况,从而导致有些任务不能在到达延时时间后立刻执行。
-
案例参考
import { taskpool } from '@kit.ArkTS';
@Concurrent
function concurrentTask(num: number): number {
console.log('这里添加需延时执行的任务');
return num;
}
// 创建任务
let task: taskpool.Task = new taskpool.Task(concurrentTask, 100);
// 延时执行task
taskpool.executeDelayed(3000, task, taskpool.Priority.HIGH).then((value: Object) => {
// 处理延时任务返回的结果
console.log("taskpool result: " + value);
});
-
与业界方案特殊差异说明
业界大多提供了任务延时调度功能,与TaskPool中的任务延时调度无明显差异。
-
不推荐应用实现方式
非必须场景不建议使用任务延时调度,防止延时结果处理时机不当。
更多推荐
所有评论(0)