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(生成缩略图),它们都依赖于任务A
    ffrt_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;
}

代码解读

  1. 依赖构建 :使用 ffrt_dependence_tffrt_deps_t 结构体来显式定义任务间的依赖关系。例如,任务B和C的依赖列表 dTaskA 中包含了任务A的句柄,这意味着它们必须等A执行完后才开始。

  2. 并行性 :由于任务B和C都只依赖A,且彼此没有依赖,FFRT运行时可以自动让它们并行执行,从而高效利用多核CPU。

  3. 同步等待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;
}

代码解读

  1. 数据签名 :依赖关系不再通过任务句柄,而是通过数据的地址(如 &data->input)来标识。FFRT会跟踪这些数据的生产者和消费者。

  2. 依赖类型

    • ffrt_dep_type_in:表示本任务需要读取该数据。

    • ffrt_dep_type_out:表示本任务会修改或生产该数据。

  3. 自动同步 :运行时根据数据依赖自动确保:当计算 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;
}

代码解读

  1. 顺序保证 :通过 ffrt_queue_attr_set_type(attr, ffrt_queue_serial) 设置串行队列,所有提交到此队列的任务都会严格按照先进先出(FIFO) 的顺序执行。

  2. 数据安全 :因为同一时间只有一个任务在执行,所以对 shared_counter 的修改是绝对安全的,无需使用锁,简化了编程模型,避免了死锁风险。

开发注意事项

  1. 任务粒度 :FFRT适合调度任务粒度不小于100微秒的计算任务。任务过小,调度开销占比会变大;任务过大,则无法充分利用系统并行度。

  2. 依赖管理 :务必正确设置任务依赖,否则可能导致数据竞争或任务无法执行。对于不再使用的任务句柄,记得使用 ffrt_task_handle_destroy 进行销毁。

  3. 等待与同步ffrt_wait() 是一个强大的同步原语,它会等待调用者提交的所有任务(包括子孙任务)都完成。在需要协调多个任务结果的场景下非常有用。

希望以上样例能帮助你直观地理解并上手鸿蒙NEXT的FFRT开发。在实际项目中,灵活运用这些并发范式,可以极大地提升应用程序的性能和响应能力。

相关推荐
程序员潘Sir4 小时前
鸿蒙应用开发从入门到实战(二十二):使用Stack实现层叠布局
harmonyos
深海的鲸同学 luvi4 小时前
【HarmonyOS】原生 Markdown 渲染解决方案 —— @luvi/lv-markdown-in
华为·harmonyos·markdown·原生渲染
2501_919749034 小时前
鸿蒙:将项目的rawfile目录下全部文件拷贝到app沙箱目录
华为·harmonyos
前端世界5 小时前
从零搭建鸿蒙高效数据存储框架:RdbStore全流程实战与性能优化
华为·性能优化·harmonyos
大霞上仙5 小时前
通过hdc 安装 .hap 到鸿蒙手机
华为·harmonyos
前端世界6 小时前
从零构建鸿蒙高效数据恢复工具:完整实战教程与可运行Demo
华为·harmonyos
郝晨妤6 小时前
【鸿蒙5.0】Scroll左右滑动
华为od·华为·harmonyos·鸿蒙
Georgewu7 小时前
【HarmonyOS Bug踩坑】主窗口调用的接口,UI在子窗口异常显示
harmonyos