在 ArkTS 的并发模型中,虽然 TaskPool 和 Worker 提供了强大的多线程能力,但“大量异步任务同时执行”并非没有代价。如果缺乏治理,应用会从“流畅”迅速转向“卡顿”甚至“崩溃”。
1. 大量异步任务同时执行可能带来的问题
-
内存压力与 OOM (Out of Memory) :
每个异步任务在挂起或执行时都需要占用闭包内存、参数拷贝内存。如果是
TaskPool任务,跨线程传递大数据会产生大量的序列化副本。任务过多会触发频繁的 GC,甚至直接导致应用被系统杀掉。 -
线程池饱和与调度延迟:
TaskPool的底层线程数是有限的(通常与 CPU 核心数相关)。当任务塞满队列,新发起的紧急 UI 任务(如点击反馈)必须排队,导致明显的交互延迟。 -
主线程“回调淹没” :
虽然任务在后台跑,但所有任务完成后的
Promise.then()或callback都会回到主线程执行。如果一秒内有数千个任务同时返回,主线程会因为处理这些回调而无法执行渲染指令,造成掉帧。 -
系统资源争抢 (CPU/IO) :
大量并发任务会争抢 CPU 时间片和磁盘 I/O 读写带宽,导致整体执行效率下降,设备发热严重。
2. 如何控制并发数量?
在 ArkTS 中,控制并发主要有三种手段:
A. 使用 Promise 队列(信号量机制)
这是最通用的做法。通过一个计数器限制同时处于 Pending 状态的 Promise 数量。
TypeScript
async function limitConcurrency(tasks: (() => Promise<any>)[], limit: number) {
const results = [];
const executing = [];
for (const task of tasks) {
const p = task().then(res => {
executing.splice(executing.indexOf(p), 1);
return res;
});
executing.push(p);
results.push(p);
if (executing.length >= limit) {
await Promise.race(executing); // 达到上限,等待其中一个完成
}
}
return Promise.all(results);
}
B. 利用 TaskPool 的任务组 (TaskGroup)
如果你有一批相关的任务,可以使用 taskpool.TaskGroup。虽然它本身不直接限制物理并发数,但你可以分批(Batching)将任务加入组中执行,从而人为控制节奏。
C. 串行执行器 (SequenceRunner)
对于必须按顺序且同一时间只能执行一个的任务,使用 taskpool.SequenceRunner。它会自动帮你排队,确保并发数始终为 1。
3. 是否可以做任务合并?
完全可以,且这是性能优化的“高阶招式”。
A. 数据侧合并 (Batching)
不要发 1000 个任务去数据库写 1000 条数据,而是发 1 个任务去写 1000 条数据的数组。
- 收益:减少了 999 次跨线程通信的开销和线程上下文切换。
B. 请求侧合并 (Debounce / Throttle)
对于高频触发的任务(如搜索框输入、滑动时的实时计算):
- 防抖 (Debounce) :用户停止输入 300ms 后再发任务。
- 节流 (Throttle) :每隔 100ms 最多执行一次任务。
C. 结果侧合并 (Promise Memoization)
如果多个组件同时请求同一个耗时计算的结果,可以建立一个单例缓存。
- 第一个请求发起时,记录该
Promise。 - 后续请求直接返回同一个
Promise实例,而不是开启新任务。
总结:大型项目的并发治理清单
| 优化维度 | 推荐手段 |
|---|---|
| 控制带宽 | 限制最大并发数(如 3-5 个),防止资源耗尽。 |
| 减少通信 | 任务尽量“大颗粒化”,合并零散的小任务。 |
| 区分优先级 | 核心 UI 任务用 Priority.HIGH,预加载任务用 Priority.LOW。 |
| 异步转同步 | 顺序依赖明显的任务改用 await 链式调用。 |
架构师建议:
在 ArkUI 中,并发不是越多越好。**“恰到好处的并发”**是让 CPU 保持在 80% 左右的平稳负载,而不是瞬间飙升到 100% 后陷入长时间的排队阻塞。