FFRT的核心并发范式与样例概览
在深入了解代码前,我们先通过一个表格来梳理FFRT支持的三种核心并发范式及其适用场景,这有助于你根据实际需求选择合适的模型。
并发范式 | 核心特点 | 适用场景 |
---|---|---|
图依赖并发 | 通过任务依赖关系构建有向无环图(DAG),运行时自动调度 | 任务间存在明确的先后顺序或复杂的依赖关系 |
串行队列 | 任务按提交顺序依次执行,保证执行顺序 | 需要避免资源竞争、确保任务顺序执行的场景 |
并发队列 | 多个任务同时执行,充分利用多核资源 | 任务间相互独立,需要提升并发性能的场景 |
样例一:图依赖并发——流媒体视频处理
这是一个非常经典的任务依赖案例,模拟视频上传后的处理流程:解析 → (转码 & 生成缩略图) → 添加水印 → 发布。其中转码和生成缩略图可以并行执行。
c
#include <stdio.h> #include "ffrt/ffrt.h"// 定义各个处理任务函数 void func_TaskA(void* arg) { printf("视频解析\n"); } void func_TaskB(void* arg) { printf("视频转码\n"); } void func_TaskC(void* arg) { printf("视频生成缩略图\n"); } void func_TaskD(void* arg) { printf("视频添加水印\n"); } void func_TaskE(void* arg) { printf("视频发布\n"); }int main() {// 提交任务A(视频解析),它没有前置依赖ffrt_task_handle_t hTaskA = ffrt_submit_h_f(func_TaskA, NULL, NULL, NULL, NULL, ffrt_auto_destroy_no);// 提交任务B(视频转码)和任务C(生成缩略图),它们都依赖于任务Affrt_dependence_t taskA_deps[] = {{ffrt_dependence_task, hTaskA}};ffrt_deps_t dTaskA = {1, taskA_deps};ffrt_task_handle_t hTaskB = ffrt_submit_h_f(func_TaskB, NULL, &dTaskA, NULL, NULL, ffrt_auto_destroy_no);ffrt_task_handle_t hTaskC = ffrt_submit_h_f(func_TaskC, NULL, &dTaskA, NULL, NULL, ffrt_auto_destroy_no);// 提交任务D(添加水印),它依赖于任务B和任务C的完成ffrt_dependence_t taskBC_deps[] = {{ffrt_dependence_task, hTaskB}, {ffrt_dependence_task, hTaskC}};ffrt_deps_t dTaskBC = {2, taskBC_deps};ffrt_task_handle_t hTaskD = ffrt_submit_h_f(func_TaskD, NULL, &dTaskBC, NULL, NULL, ffrt_auto_destroy_no);// 提交任务E(视频发布),它依赖于任务D的完成ffrt_dependence_t taskD_deps[] = {{ffrt_dependence_task, hTaskD}};ffrt_deps_t dTaskD = {1, taskD_deps};ffrt_submit_f(func_TaskE, NULL, &dTaskD, NULL, NULL);// 等待所有任务执行完毕ffrt_wait();// 手动销毁任务句柄ffrt_task_handle_destroy(hTaskA);ffrt_task_handle_destroy(hTaskB);ffrt_task_handle_destroy(hTaskC);ffrt_task_handle_destroy(hTaskD);return 0; }
代码解读:
-
依赖构建:使用
ffrt_dependence_t
和ffrt_deps_t
结构体来显式定义任务间的依赖关系。例如,任务B和C的依赖列表dTaskA
中包含了任务A的句柄,这意味着它们必须等A执行完后才开始。 -
并行性:由于任务B和C都只依赖A,且彼此没有依赖,FFRT运行时可以自动让它们并行执行,从而高效利用多核CPU。
-
同步等待:
ffrt_wait()
会阻塞主线程,直到整个任务图中的所有任务都执行完成,确保流程的完整性。
样例二:数据依赖——斐波那契数列计算
这个例子展示了如何用数据依赖而非任务句柄来表达任务间的关系,非常适合计算有向无环图结构的数值,比如斐波那契数列 F(n) = F(n-1) + F(n-2)
。
c
#include <stdio.h> #include "ffrt/ffrt.h"typedef struct {int input;int* output; } fib_data_t;void fib_task(void* arg) {fib_data_t* data = (fib_data_t*)arg;int n = data->input;int* result = data->output;if (n <= 1) {*result = n; // 递归基} else {int result1, result2;fib_data_t data1 = {n - 1, &result1};fib_data_t data2 = {n - 2, &result2};// 定义数据依赖:当前任务消耗input数据,产出output数据ffrt_dependence_t in_deps[] = {{ffrt_dependence_data, &data->input, ffrt_dep_type_in}};ffrt_dependence_t out_deps[] = {{ffrt_dependence_data, data->output, ffrt_dep_type_out}};ffrt_deps_t deps_in = {1, in_deps};ffrt_deps_t deps_out = {1, out_deps};// 提交子任务,计算F(n-1)和F(n-2)ffrt_submit_f(fib_task, &data1, &deps_in, &deps_out, NULL, ffrt_task_attr_default);ffrt_submit_f(fib_task, &data2, &deps_in, &deps_out, NULL, ffrt_task_attr_default);// 等待两个子任务完成并汇总结果ffrt_wait();*result = result1 + result2;} }int main() {int n = 10;int final_result;fib_data_t main_data = {n, &final_result};// 计算F(10)ffrt_submit_f(fib_task, &main_data, NULL, NULL, NULL, ffrt_task_attr_default);ffrt_wait(); // 等待整个计算过程完成printf("F(%d) = %d\n", n, final_result);return 0; }
代码解读:
-
数据签名:依赖关系不再通过任务句柄,而是通过数据的地址(如
&data->input
)来标识。FFRT会跟踪这些数据的生产者和消费者。 -
依赖类型:
-
ffrt_dep_type_in
:表示本任务需要读取该数据。 -
ffrt_dep_type_out
:表示本任务会修改或生产该数据。
-
-
自动同步:运行时根据数据依赖自动确保:当计算
F(n)
时,其依赖的F(n-1)
和F(n-2)
必定已经计算完成。这完美体现了 Producer-Consumer 依赖关系。
样例三:串行队列——顺序访问共享资源
当多个任务需要按顺序访问某个共享资源(如一个配置文件、一段内存或一个硬件设备)时,串行队列是最简单可靠的选择。
c
#include <stdio.h> #include "ffrt/ffrt.h"// 模拟一个共享资源 int shared_counter = 0;void task_increment(void* arg) {// 此任务在串行队列中执行,无需加锁shared_counter++;printf("Task Increment: shared_counter is now %d\n", shared_counter); }void task_double(void* arg) {shared_counter *= 2;printf("Task Double: shared_counter is now %d\n", shared_counter); }int main() {// 创建一个串行队列属性ffrt_queue_attr_t attr;ffrt_queue_attr_init(&attr);ffrt_queue_attr_set_type(attr, ffrt_queue_serial);// 将任务提交到同一个串行队列中for (int i = 0; i < 5; i++) {// 任务将按for循环的提交顺序依次执行ffrt_submit_f(task_increment, NULL, NULL, NULL, &attr, ffrt_task_attr_default);ffrt_submit_f(task_double, NULL, NULL, NULL, &attr, ffrt_task_attr_default);}ffrt_wait();printf("Final value: %d\n", shared_counter);ffrt_queue_attr_destroy(attr);return 0; }
代码解读:
-
顺序保证:通过
ffrt_queue_attr_set_type(attr, ffrt_queue_serial)
设置串行队列,所有提交到此队列的任务都会严格按照先进先出(FIFO) 的顺序执行。 -
数据安全:因为同一时间只有一个任务在执行,所以对
shared_counter
的修改是绝对安全的,无需使用锁,简化了编程模型,避免了死锁风险。
开发注意事项
-
任务粒度:FFRT适合调度任务粒度不小于100微秒的计算任务。任务过小,调度开销占比会变大;任务过大,则无法充分利用系统并行度。
-
依赖管理:务必正确设置任务依赖,否则可能导致数据竞争或任务无法执行。对于不再使用的任务句柄,记得使用
ffrt_task_handle_destroy
进行销毁。 -
等待与同步:
ffrt_wait()
是一个强大的同步原语,它会等待调用者提交的所有任务(包括子孙任务)都完成。在需要协调多个任务结果的场景下非常有用。
希望以上样例能帮助你直观地理解并上手鸿蒙NEXT的FFRT开发。在实际项目中,灵活运用这些并发范式,可以极大地提升应用程序的性能和响应能力。