明明创建了 8 个任务,为什么 FFRT 并没有简单地开 8 个线程同时执行?

文章封面

做并发编程的时候,最开始的思路很简单:有 8 个任务,开 8 个线程,不就跑完了?

结果真用了 FFRT 之后发现不对:提交了 8 个任务,运行时并没有真的开 8 个线程同时跑。有的任务要等,有的任务并行跑,调度完全不是你想的那样。

这时候才意识到:FFRT 不是普通的线程池。它的核心不是"开多少线程",而是"把程序描述成一张任务图,然后自动调度"。

一、先写四个任务,看看它们到底怎么跑

先写四个任务,A、B、C、D,每个都打印一下开始和结束时间。

#include <ffrt/ffrt.hpp>

int main() {
  ffrt::submit([]{ 
    printf("A start\n"); 
    sleep(1); 
    printf("A end\n"); 
  });
  
  ffrt::submit([]{ 
    printf("B start\n"); 
    sleep(1); 
    printf("B end\n"); 
  });
  
  ffrt::submit([]{ 
    printf("C start\n"); 
    sleep(1); 
    printf("C end\n"); 
  });
  
  ffrt::submit([]{ 
    printf("D start\n"); 
    sleep(1); 
    printf("D end\n"); 
  });
}

跑一下,结果是什么?A、B、C、D 真的同时开始吗?

不一定。FFRT 会根据 CPU 核心数和系统负载来调度。比如 4 核 CPU,可能真的同时跑 4 个。但如果系统还有别的进程在跑,可能就只跑 2 个,剩下的排队。

二、任务依赖是什么意思

现在加个需求:B 必须等 A 跑完才能开始。

怎么实现?用 in_deps 和 out_deps。

概念作用
in_deps这个任务要等哪些任务完成才能开始
out_deps这个任务完成后,哪些任务可以开始

依赖是通过数据地址建立的。不是用任务 ID,是用你要读写的数据的地址。

这段代码解决什么问题: 建立任务依赖关系。
文件: ffrt/task_demo.cpp
用途: 任务图调度
接入位置: Native 并发任务

int data = 0;

// A 任务:写 data
ffrt::submit({
  .deps = {.out_deps = {&data}}
}, []{
  data = 42;
});

// B 任务:读 data,要等 A 写完
ffrt::submit({
  .deps = {.in_deps = {&data}}
}, []{
  printf("data = %d\n", data);
});

这里 A 的 out_deps 是 &data,B 的 in_deps 也是 &data。FFRT 看到它们依赖同一个地址,就知道 B 要等 A 跑完。

系统架构图

三、任务图是怎么形成的

把多个任务和依赖放在一起,就形成了一张任务图。

比如:A 和 B 互相不依赖,C 要等 A,D 要等 B,E 要等 C 和 D。

A ──> C ──┐
          ├──> E
B ──> D ──┘

FFRT 会自动分析这张图:A 和 B 没有依赖,可以并行跑。C 要等 A,D 要等 B。E 要等 C 和 D 都完成。

这就是任务图的核心:不是你决定先跑谁后跑谁,是依赖关系决定的。

四、QoS 和任务优先级是什么

任务不只是有依赖,还有优先级。QoS 就是服务等级。

QoS 等级适合的任务
高优先级UI 响应、交互相关
中优先级普通业务计算
低优先级后台任务、预加载

QoS 高的任务,会优先被调度。不是说低优先级的不跑,是高优先级的先跑。

你还可以在任务运行的时候动态调整 QoS。比如一个任务开始是后台的,后来用户切换到前台了,就把它的 QoS 调高。

五、最大并发度是什么意思

FFRT 有个参数叫 max_concurrency,就是最大同时跑多少个任务。

为什么要限制?因为开太多线程反而慢。线程切换有开销,CPU 就那么多核心,开太多线程,大部分时间都在切换,反而做不了事。

并发度效果
太小任务排队,跑不完
合适刚好跑满 CPU
太大线程切换开销大,反而慢

所以不是并发度越大越好。合适才重要。

运行效果图

六、ffrt_wait 是什么意思

提交了任务,怎么等它完成?用 ffrt_wait。

// 提交任务,拿到 handle
auto handle = ffrt::submit({
  .deps = {.out_deps = {&data}}
}, []{
  data = 42;
});

// 等这个任务完成
ffrt::wait(handle);

wait 会阻塞当前线程,直到任务完成。但注意:不要在任务内部 wait 另一个任务,这样会造成调度效率下降,甚至死锁。

七、几个容易踩的坑

第一个坑:把 FFRT 当成普通线程池。它不是,它是任务图调度。

第二个坑:随便拿 NULL 或固定数字当依赖标识。依赖是数据地址,不是随便填的。

第三个坑:两个任务实际访问同一份数据却没有建立依赖。两个任务同时写同一块内存,就出问题了。

第四个坑:依赖范围过大导致本来能并行的任务全部串行。in_deps 填太多,本来能并行的都变成串行。

第五个坑:盲目提高 QoS。所有任务都设最高优先级,等于没有优先级。

第六个坑:任务内部再次阻塞等待造成调度效率下降。任务里 wait 另一个任务,调度器就少了一个 worker。

第七个坑:把任务数量等同于线程数量。提交 100 个任务,不代表开 100 个线程。

业务流程图

这次做 FFRT 最大的体会是:FFRT 的核心不是线程池,是任务图。你不是在管理线程,你是在描述任务之间的依赖关系,调度器自动帮你决定怎么跑。

真正做的时候,最容易忽略的不是 API 怎么调,而是依赖怎么设计。依赖设计对了,任务自动并行。依赖设计错了,本来能并行的全部串行,性能上不去。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐