
文章目录
-
-
- 一、状态变量设计:围绕"心跳"构建的观测体系
- 二、HeartbeatSection:整个页面的"灵魂"
- 三、BenchmarkSection:三种执行模式的对比
- 四、ActionSection:四档测试的渐进式设计
-
- 颜色编码的语义层次
- [`isBenchmarking` 的互斥锁](#
isBenchmarking的互斥锁) - "全套基准"为什么是推荐的
- 五、DecisionSection:决策矩阵
- 六、WorkflowSection:集成步骤
- 七、与前两个页面的架构对比
- 八、设计亮点总结
- 完整代码
-
一、状态变量设计:围绕"心跳"构建的观测体系
| 变量 | 类型 | 角色 | 核心意义 |
|---|---|---|---|
concurrencyStatus |
string | 并发状态描述 | 低------文本标签 |
isBenchmarking |
boolean | 是否正在跑分 | 中------控制所有按钮的可用性 |
uiHeartbeat |
number | 心跳计数器 | 高------每 100ms 自增,主线程阻塞时停止 |
benchmarkResults |
ConcurrencyBenchmarkResult\[\] | 跑分结果集 | 低------仅跑分后更新 |
concurrencyLogs |
ConcurrencyLogItem\[\] | 并发日志 | 中 |
lastHeartbeatDelta |
number | 基准期间心跳增量 | 低------跑分后更新 |
与前两个页面的本质区别
前两个页面(二进制流、Native NAPI)的状态变量都是围绕数据 设计的------解析了多少包、跑分结果是什么。而这个页面的核心状态变量 uiHeartbeat 是围绕时间设计的------它不关心数据内容,只关心"主线程还活着吗"。
这是一种元观测(meta-observation):不是在测量任务本身,而是在测量"执行任务时系统的响应能力"。
二、HeartbeatSection:整个页面的"灵魂"
typescript
Text(`心跳 ${this.uiHeartbeat}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor(this.isBenchmarking ? '#FBBF24' : '#6EE7B7')
心跳机制的工作原理
一个定时器每 100ms 向 uiHeartbeat 加 1。由于 ArkTS 的 UI 更新运行在主线程上,这个自增操作必须等待主线程空闲才能执行。因此:
- 主线程空闲:心跳正常递增,数字持续增长。
- 主线程被阻塞(例如执行一个同步的 500ms 循环):心跳暂停,数字冻结。
- TaskPool 执行:重计算在 Worker 线程进行,主线程保持空闲,心跳继续递增。
这就是为什么 lastHeartbeatDelta 如此重要:
typescript
Text(`最近基准期间 +${this.lastHeartbeatDelta} tick`)
它记录的是一次跑分期间心跳增加了多少。如果增量是 0,说明主线程被完全阻塞;如果增量很大,说明主线程始终畅通。
颜色语义
typescript
.fontColor(this.isBenchmarking ? '#FBBF24' : '#6EE7B7')
- 绿色 (
#6EE7B7):空闲状态,系统健康。 - 黄色 (
#FBBF24):跑分进行中,心跳数字正在被"考验"。
黄色是一种"警告色"------暗示"注意观察这个数字的变化"。
aboutToDisappear 的生命周期管理
typescript
aboutToDisappear(): void {
TaskPoolService.stopHeartbeat()
}
这是整个页面中唯一的生命周期钩子。心跳定时器是一个全局资源,如果页面销毁后不停止,定时器会继续运行,浪费 CPU 并可能导致内存泄漏(定时器回调引用了已销毁的组件状态)。
对比前两个页面------二进制流页面有 startStream/stopStream,Native 页面没有显式清理(因为 NAPI 模块是全局单例)。只有并发页面需要显式管理定时器生命周期,因为心跳是一个持续运行的后台任务。
三、BenchmarkSection:三种执行模式的对比
跑分结果的数据结构
typescript
ForEach(this.benchmarkResults, (item: ConcurrencyBenchmarkResult) => {
Text(item.mode) // "主线程" / "TaskPool 单任务" / "TaskPool 并行"
Text(`${item.durationMs}ms`) // 耗时
Text(`心跳 +${item.uiHeartbeatDelta}`) // 期间心跳增量
Text(item.output) // 计算结果
Text(item.recommendation) // 推荐建议
})
与 Native 页面的跑分对比,关键差异在于多了一个 uiHeartbeatDelta 字段。Native 页面关心的是"谁算得更快",而这个页面关心的是"谁不阻塞主线程"。
心跳增量的颜色编码
typescript
Text(`心跳 +${item.uiHeartbeatDelta}`)
.fontColor(item.uiHeartbeatDelta > 0 ? '#6EE7B7' : '#FCA5A5')
- 绿色(增量 > 0):主线程未被阻塞,UI 保持响应。
- 红色(增量 = 0):主线程被完全阻塞,UI 冻结。
这是整个页面最直观的教学工具------开发者一眼就能看到:主线程跑重任务时数字是红色的 0,TaskPool 跑同样的任务时数字是绿色的几十。
recommendation 字段的设计
typescript
Text(item.recommendation)
.fontSize(10)
.fontColor('#475569')
每条跑分结果自带一条推荐建议,例如:
- 主线程:"❌ 阻塞主线程,UI 冻结"
- TaskPool 单任务:"✓ 主线程空闲,推荐用于中等计算量"
- TaskPool 并行:"✓ 最快完成,推荐用于大规模并行计算"
这些建议不是硬编码在 UI 层,而是由 TaskPoolService 在跑分完成后根据实际数据动态生成的,确保建议与数据一致。
四、ActionSection:四档测试的渐进式设计
┌──────────────────────────────┐
│ 全套基准(推荐) │ ← 蓝色,一键跑全部
├──────────────┬───────────────┤
│ 主线程 │ TaskPool │ ← 红色 vs 绿色,对比鲜明
├──────────────┼───────────────┤
│ 并行TaskGroup │ 轻量开销 │ ← 紫色 vs 灰色,进阶选项
└──────────────┴───────────────┘
颜色编码的语义层次
| 按钮 | 颜色 | 语义 |
|---|---|---|
| 全套基准 | #2563EB 蓝色 |
主操作,推荐入口 |
| 主线程 | #DC2626 红色 |
"危险"操作,会阻塞 UI |
| TaskPool | #059669 绿色 |
"安全"操作,不阻塞 UI |
| 并行 TaskGroup | #6366F1 紫色 |
进阶操作,多任务并行 |
| 轻量开销 | #334155 灰色 |
辅助操作,测量 TaskPool 本身的调度开销 |
红色和绿色的对比不是随意的------它在暗示"主线程跑重任务是有害的"。
isBenchmarking 的互斥锁
typescript
Button('全套基准(推荐)')
.enabled(!this.isBenchmarking)
所有按钮都受 isBenchmarking 控制。这是一个互斥锁 ------防止用户在跑分进行中重复触发。由于跑分涉及异步操作(taskpool.execute 返回 Promise),如果不加锁,用户快速点击可能导致多个跑分任务同时执行,心跳数据互相干扰。
"全套基准"为什么是推荐的
单独跑"主线程"或"TaskPool"只能看到一半画面。全套基准会按顺序执行:
- 主线程重任务 → 观察心跳冻结
- TaskPool 单任务 → 观察心跳正常
- TaskPool 并行 → 观察心跳正常且更快
- 轻量开销 → 观察 TaskPool 调度本身的成本
四步下来,开发者能完整理解"什么时候用、用多少、代价是什么"。
五、DecisionSection:决策矩阵
这是整个页面最具实用价值的部分。
正向决策表
typescript
ForEach(TASKPOOL_DECISIONS, (item: TaskPoolDecisionItem) => {
Text(item.useTaskPool ? '✓' : '✗')
Text(item.scenario)
Text(item.reason)
})
TASKPOOL_DECISIONS 是一个决策矩阵,典型的条目可能包括:
| ✓/✗ | 场景 | 原因 |
|---|---|---|
| ✓ | 图片解码 / 压缩 | CPU 密集,阻塞主线程会导致掉帧 |
| ✓ | JSON 大文件解析 | 解析耗时不可预测,可能超过 16ms 帧预算 |
| ✓ | 加密 / 哈希计算 | 纯计算,无 UI 依赖 |
| ✗ | DOM 操作 / UI 更新 | TaskPool 线程无法访问 UI 组件 |
| ✗ | 简单数组排序(< 1000 项) | 调度开销 > 计算收益 |
| ✗ | 需要频繁回调 UI 的流式处理 | 跨线程通信开销抵消并行收益 |
反模式
typescript
ForEach(TASKPOOL_ANTIPATTERNS, (tip: string) => {
Text(`· ${tip}`)
})
反模式列表通常包括:
- 在 TaskPool 中操作 UI 组件:Worker 线程没有 UI 上下文,会直接崩溃。
- 传递不可序列化的对象 :TaskPool 通过结构化克隆传递参数,
Function、DOM 节点等无法序列化。 - 用 TaskPool 包装 1ms 的小任务:调度开销(创建 Worker、序列化参数、反序列化结果)可能远大于任务本身。
- 在 TaskPool 中访问 AppStorage :
@StorageLink绑定的是主线程状态,Worker 线程无法直接访问。
六、WorkflowSection:集成步骤
typescript
ForEach(TASKPOOL_WORKFLOW, (step: string, index: number) => {
Text(`${index + 1}. ${step}`)
})
典型的 TaskPool 集成步骤:
- 标记函数为
@Concurrent:告诉编译器该函数可以在 Worker 线程执行。 - 确保函数参数和返回值可序列化:使用基本类型、Plain Object、ArrayBuffer 等。
- 调用
taskpool.execute():将任务提交到 TaskPool。 - 处理返回的 Promise :通过
.then()或await获取结果。 - 在主线程更新 UI :将结果写回
@State/@StorageLink变量。
七、与前两个页面的架构对比
| 维度 | 二进制流页面 | Native NAPI 页面 | TaskPool 页面 |
|---|---|---|---|
| 核心问题 | 数据解析正确性 | 跨语言性能差异 | 主线程是否被阻塞 |
| 观测手段 | Hex 预览 + 包结构 | 红绿进度条 | UI 心跳计数器 |
| 数据流 | 持续流入 | 离散触发 | 离散触发 + 持续心跳 |
| 错误模型 | 解析错误计数 | nativeAvailable 开关 |
isBenchmarking 互斥锁 |
| 教学价值 | 二进制协议 | NAPI 集成 | 并发决策 |
| 生命周期管理 | 有 start/stop | 无显式清理 | aboutToDisappear 停止心跳 |
八、设计亮点总结
心跳作为"活体检测"
心跳机制是这个页面最精妙的设计。它将一个抽象概念("主线程阻塞")转化为一个具体的、可感知的数字。开发者不需要理解事件循环、消息队列、帧预算等底层概念,只需要看数字------数字在动就是好的,数字停了就是坏的。
决策矩阵优于性能数字
Native 页面的核心输出是"加速比"(一个数字),而这个页面的核心输出是"何时用 TaskPool"(一张决策表)。性能数字会随硬件变化,但决策逻辑是稳定的------"图片解码应该放 TaskPool"这个结论在任何设备上都是对的。
反模式与正向建议并列
DecisionSection 同时展示了"该做什么"和"不该做什么"。这种正反对比的教学方式比单纯列出最佳实践更有效------开发者不仅知道正确的做法,还知道常见的错误陷阱。
轻量开销测试的存在
"轻量开销"按钮测量的是 TaskPool 调度本身的成本。这防止了一种常见误解------"TaskPool 总是更快"。通过展示调度开销,开发者能理解为什么小任务不值得用 TaskPool,从而做出更精确的决策。
完整代码
dart
import { TaskPoolService } from '../concurrency/TaskPoolService'
import {
ConcurrencyBenchmarkResult,
ConcurrencyLogItem,
HEAVY_ITERATIONS,
PARALLEL_ITERATIONS,
PARALLEL_TASK_COUNT,
TASKPOOL_ANTIPATTERNS,
TASKPOOL_DECISIONS,
TASKPOOL_WORKFLOW,
TaskPoolDecisionItem
} from '../concurrency/ConcurrencyTypes'
@Entry
@Component
struct Index {
@StorageLink('concurrencyStatus') concurrencyStatus: string = 'TaskPool 并发就绪'
@StorageLink('isBenchmarking') isBenchmarking: boolean = false
@StorageLink('uiHeartbeat') uiHeartbeat: number = 0
@StorageLink('benchmarkResults') benchmarkResults: ConcurrencyBenchmarkResult[] = []
@StorageLink('concurrencyLogs') concurrencyLogs: ConcurrencyLogItem[] = []
@StorageLink('lastHeartbeatDelta') lastHeartbeatDelta: number = 0
aboutToDisappear(): void {
TaskPoolService.stopHeartbeat()
}
build() {
Scroll() {
Column({ space: 16 }) {
this.HeaderSection()
this.HeartbeatSection()
this.BenchmarkSection()
this.ActionSection()
this.DecisionSection()
this.WorkflowSection()
this.LogSection()
}
.width('100%')
.padding(16)
}
.width('100%')
.height('100%')
.backgroundColor('#0B0F17')
}
@Builder
HeaderSection() {
Column({ space: 6 }) {
Text('多线程并发 · TaskPool 最佳时机')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.fontColor('#F8FAFC')
Text(this.concurrencyStatus)
.fontSize(13)
.fontColor('#94A3B8')
Text('@Concurrent + taskpool.execute · 计算密集 vs UI 心跳')
.fontSize(11)
.fontColor('#64748B')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
}
@Builder
HeartbeatSection() {
Column({ space: 8 }) {
Text('UI 心跳监测(100ms tick)')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#E2E8F0')
Row({ space: 16 }) {
Text(`心跳 ${this.uiHeartbeat}`)
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor(this.isBenchmarking ? '#FBBF24' : '#6EE7B7')
Column({ space: 4 }) {
Text(`最近基准期间 +${this.lastHeartbeatDelta} tick`)
.fontSize(12)
.fontColor('#94A3B8')
Text('主线程阻塞时 tick 停止 · TaskPool 期间持续增长')
.fontSize(10)
.fontColor('#64748B')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
}
.width('100%')
.padding(14)
.backgroundColor('#121826')
.borderRadius(14)
}
@Builder
BenchmarkSection() {
Column({ space: 8 }) {
Text('基准结果')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#E2E8F0')
Text(`重任务 ${HEAVY_ITERATIONS} 次循环 · 并行 ${PARALLEL_TASK_COUNT}x${PARALLEL_ITERATIONS}`)
.fontSize(11)
.fontColor('#64748B')
if (this.benchmarkResults.length === 0) {
Text('点击「全套基准」或单项测试')
.fontSize(12)
.fontColor('#64748B')
} else {
ForEach(this.benchmarkResults, (item: ConcurrencyBenchmarkResult) => {
Column({ space: 4 }) {
Row({ space: 8 }) {
Text(item.mode)
.fontSize(13)
.fontColor('#F8FAFC')
.layoutWeight(1)
Text(`${item.durationMs}ms`)
.fontSize(12)
.fontColor('#93C5FD')
}
.width('100%')
Row({ space: 12 }) {
Text(`心跳 +${item.uiHeartbeatDelta}`)
.fontSize(11)
.fontColor(item.uiHeartbeatDelta > 0 ? '#6EE7B7' : '#FCA5A5')
Text(item.output)
.fontSize(10)
.fontColor('#64748B')
}
Text(item.recommendation)
.fontSize(10)
.fontColor('#475569')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(10)
.backgroundColor('#111827')
.borderRadius(8)
}, (item: ConcurrencyBenchmarkResult) => item.mode)
}
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(14)
.backgroundColor('#121826')
.borderRadius(14)
}
@Builder
ActionSection() {
Column({ space: 10 }) {
Button('全套基准(推荐)')
.width('100%')
.backgroundColor('#2563EB')
.enabled(!this.isBenchmarking)
.onClick(() => {
TaskPoolService.runFullSuite()
})
Row({ space: 10 }) {
Button('主线程')
.layoutWeight(1)
.backgroundColor('#DC2626')
.enabled(!this.isBenchmarking)
.onClick(() => {
TaskPoolService.runMainThreadBenchmark()
})
Button('TaskPool')
.layoutWeight(1)
.backgroundColor('#059669')
.enabled(!this.isBenchmarking)
.onClick(() => {
TaskPoolService.runTaskPoolSingle()
})
}
.width('100%')
Row({ space: 10 }) {
Button('并行 TaskGroup')
.layoutWeight(1)
.backgroundColor('#6366F1')
.enabled(!this.isBenchmarking)
.onClick(() => {
TaskPoolService.runTaskPoolParallel()
})
Button('轻量开销')
.layoutWeight(1)
.backgroundColor('#334155')
.enabled(!this.isBenchmarking)
.onClick(() => {
TaskPoolService.runTaskPoolLightOverhead()
})
}
.width('100%')
}
.width('100%')
}
@Builder
DecisionSection() {
Column({ space: 8 }) {
Text('何时用 TaskPool')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#E2E8F0')
ForEach(TASKPOOL_DECISIONS, (item: TaskPoolDecisionItem) => {
Row({ space: 8 }) {
Text(item.useTaskPool ? '✓' : '✗')
.fontColor(item.useTaskPool ? '#6EE7B7' : '#FCA5A5')
Column({ space: 2 }) {
Text(item.scenario)
.fontSize(12)
.fontColor('#E2E8F0')
Text(item.reason)
.fontSize(10)
.fontColor('#64748B')
}
.alignItems(HorizontalAlign.Start)
.layoutWeight(1)
}
.width('100%')
.padding({ top: 4, bottom: 4 })
}, (item: TaskPoolDecisionItem) => item.id)
Text('反模式')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#E2E8F0')
.margin({ top: 8 })
ForEach(TASKPOOL_ANTIPATTERNS, (tip: string) => {
Text(`· ${tip}`)
.fontSize(12)
.fontColor('#94A3B8')
}, (tip: string) => tip)
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(14)
.backgroundColor('#121826')
.borderRadius(14)
}
@Builder
WorkflowSection() {
Column({ space: 6 }) {
Text('TaskPool 集成步骤')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#E2E8F0')
ForEach(TASKPOOL_WORKFLOW, (step: string, index: number) => {
Text(`${index + 1}. ${step}`)
.fontSize(12)
.fontColor('#94A3B8')
}, (step: string, index: number) => `${index}_${step}`)
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(14)
.backgroundColor('#121826')
.borderRadius(14)
}
@Builder
LogSection() {
Column({ space: 8 }) {
Text('并发日志')
.fontSize(14)
.fontWeight(FontWeight.Medium)
.fontColor('#E2E8F0')
if (this.concurrencyLogs.length === 0) {
Text('暂无日志')
.fontSize(12)
.fontColor('#64748B')
} else {
ForEach(this.concurrencyLogs, (item: ConcurrencyLogItem) => {
Column({ space: 2 }) {
Row({ space: 8 }) {
Text(item.time)
.fontSize(10)
.fontColor('#64748B')
Text(item.layer)
.fontSize(11)
.fontColor('#93C5FD')
}
Text(item.message)
.fontSize(12)
.fontColor('#CBD5E1')
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding({ top: 4, bottom: 4 })
}, (item: ConcurrencyLogItem) => item.id)
}
}
.alignItems(HorizontalAlign.Start)
.width('100%')
.padding(14)
.backgroundColor('#121826')
.borderRadius(14)
}
}


九、潜在优化建议
心跳定时器的精度
100ms 的 tick 间隔意味着心跳的时间分辨率是 100ms。如果一个任务阻塞主线程 50ms,心跳可能不会暂停(恰好在阻塞间隙执行了 tick)。更精确的方案是使用 requestAnimationFrame 或更短的 tick 间隔(如 16ms),但这会增加 UI 刷新频率。100ms 是一个合理的折中。
跑分结果的持久化
当前跑分结果存储在 @StorageLink 中,页面退出后仍然保留。但如果应用被系统回收,数据会丢失。可以考虑将结果序列化到 Preferences 中,实现跨会话的跑分历史对比。
ForEach 的 key 稳定性
typescript
ForEach(this.benchmarkResults, (item: ConcurrencyBenchmarkResult) => {
// ...
}, (item: ConcurrencyBenchmarkResult) => item.mode)
使用 item.mode 作为 key 是合理的,因为模式名称("主线程"、"TaskPool 单任务"等)是唯一的。但如果未来支持同一模式多次跑分(如"主线程 #1"、"主线程 #2"),需要改为组合 key。
这个页面本质上是一个并发决策支持工具 ------它不告诉你"TaskPool 比主线程快多少倍",而是告诉你"在什么场景下该用 TaskPool,在什么场景下不该用"。这种"决策优于数字"的设计理念,使它成为整个系列中最具工程实践价值的页面。
?