1# Function Flow Runtime图依赖并发(C) 2 3## 概述 4 5FFRT图依赖并发范式支持任务依赖和数据依赖两种方式构建任务依赖图。任务依赖图中每个节点表示一个任务,边表示任务之间的依赖关系。任务依赖分为输入依赖`in_deps`和输出依赖`out_deps`。 6 7构建任务依赖图的两种不同方式: 8 9- 当使用任务依赖方式构建任务依赖图时,使用任务句柄`handle`来对应一个任务对象。 10- 当使用数据依赖方式构建任务依赖图时,数据对象表达抽象为数据签名,每个数据签名唯一对应一个数据对象。 11 12### 任务依赖 13 14> **说明:** 15> 16> 当任务句柄出现在一个任务的`in_deps`中时,任务句柄对应的任务是该任务的前置任务;当任务句柄出现在一个任务的`out_deps`中时,任务句柄对应的任务是该任务的后继任务。 17 18任务依赖适用于任务之间有明确顺序或逻辑流程要求的场景,例如: 19 20- 顺序执行的任务,例如:先进行数据预处理任务,然后再进行模型训练任务。 21- 逻辑流程控制,例如:商品交易过程中,通常是先下单,然后是制作,最后是物流运输。 22- 多级任务链,例如:流媒体视频处理过程中,视频解析后可以进行视频转码和视频生成缩略图,然后是视频添加水印,最后是视频发布。 23 24### 数据依赖 25 26> **说明:** 27> 28> 当数据对象的签名出现在一个任务的`in_deps`中时,该任务称为数据对象的消费者任务,消费者任务执行不改变其输入数据对象的内容; 29> 当数据对象的签名出现在任务的`out_deps`中时,该任务称为数据对象的生产者任务,生产者任务执行改变其输出数据对象的内容,从而生成该数据对象的一个新的版本。 30 31数据依赖适用于任务之间通过数据生产和消费关系来触发执行的场景。 32 33一个数据对象可能存在多个版本,每个版本对应一个生产者任务和零个,一个或多个消费者任务,根据生产者任务和消费者任务的下发顺序定义数据对象的多个版本的顺序,以及每个版本所对应的生产者和消费者任务。 34 35数据依赖解除的任务进入就绪状态允许被调度执行,依赖解除状态指任务所有输入数据对象版本的生产者任务执行完成,且所有输出数据对象版本的所有消费者任务执行完成的状态。 36 37FFRT在运行时可动态构建任务之间的基于生产者/消费者的数据依赖关系并遵循任务数据依赖状态执行调度,包括: 38 39- Producer-Consumer依赖 40 41 一个数据对象版本的生产者任务和该数据对象版本的消费者任务之间形成的依赖关系,也称为Read-after-Write依赖。 42 43- Consumer-Producer依赖 44 45 一个数据对象版本的消费者任务和该数据对象的下一个版本的生产者任务之间形成的依赖关系,也称为Write-after-Read依赖。 46 47- Producer-Producer依赖 48 49 一个数据对象版本的生产者任务和该数据对象的下一个版本的生产者任务之间形成的依赖关系,也称为Write-after-Write依赖。 50 51例如,存在一组任务与数据A的关系表述为: 52 53```cpp 54task1(OUT A); 55task2(IN A); 56task3(IN A); 57task4(OUT A); 58task5(OUT A); 59``` 60 61 62 63为表述方便,本文中的数据流图均以圆圈表示Task,方块表示数据。 64 65可以得出以下结论: 66 67- task1与task2/task3构成Producer-Consumer依赖,即:task2/task3需要等到task1写完A之后才能读A。 68- task2/task3与task4构成Consumer-Producer依赖,即:task4需要等到task2/task3读完A之后才能写A。 69- task4与task5构成Producer-Producer依赖,即:task5需要等到task4写完A之后才能写A。 70 71## 示例:流媒体视频处理 72 73用户上传视频到流媒体平台,处理步骤包含:视频解析A、视频转码B、视频缩略图生成C、视频水印添加D和视频发布E,其中步骤B和步骤C可以并行执行。任务流程如下图所示: 74 75 76 77借助FFRT提供了图依赖并发范式,可以描述任务依赖关系,同时并行化上述视频处理流程,代码如下所示: 78 79```c 80#include <stdio.h> 81#include "ffrt/ffrt.h" 82 83void func_TaskA(void* arg) 84{ 85 printf("视频解析\n"); 86} 87 88void func_TaskB(void* arg) 89{ 90 printf("视频转码\n"); 91} 92 93void func_TaskC(void* arg) 94{ 95 printf("视频生成缩略图\n"); 96} 97 98void func_TaskD(void* arg) 99{ 100 printf("视频添加水印\n"); 101} 102 103void func_TaskE(void* arg) 104{ 105 printf("视频发布\n"); 106} 107 108int main() 109{ 110 // 提交任务A 111 ffrt_task_handle_t hTaskA = ffrt_submit_h_f(func_TaskA, NULL, NULL, NULL, NULL); 112 113 // 提交任务B和C 114 ffrt_dependence_t taskA_deps[] = {{ffrt_dependence_task, hTaskA}}; 115 ffrt_deps_t dTaskA = {1, taskA_deps}; 116 ffrt_task_handle_t hTaskB = ffrt_submit_h_f(func_TaskB, NULL, &dTaskA, NULL, NULL); 117 ffrt_task_handle_t hTaskC = ffrt_submit_h_f(func_TaskC, NULL, &dTaskA, NULL, NULL); 118 119 // 提交任务D 120 ffrt_dependence_t taskBC_deps[] = {{ffrt_dependence_task, hTaskB}, {ffrt_dependence_task, hTaskC}}; 121 ffrt_deps_t dTaskBC = {2, taskBC_deps}; 122 ffrt_task_handle_t hTaskD = ffrt_submit_h_f(func_TaskD, NULL, &dTaskBC, NULL, NULL); 123 124 // 提交任务E 125 ffrt_dependence_t taskD_deps[] = {{ffrt_dependence_task, hTaskD}}; 126 ffrt_deps_t dTaskD = {1, taskD_deps}; 127 ffrt_submit_f(func_TaskE, NULL, &dTaskD, NULL, NULL); 128 129 // 等待所有任务完成 130 ffrt_wait(); 131 132 ffrt_task_handle_destroy(hTaskA); 133 ffrt_task_handle_destroy(hTaskB); 134 ffrt_task_handle_destroy(hTaskC); 135 ffrt_task_handle_destroy(hTaskD); 136 return 0; 137} 138``` 139 140预期的输出可能为: 141 142```plain 143视频解析 144视频转码 145视频生成缩略图 146视频添加水印 147视频发布 148``` 149 150## 示例:斐波那契数列 151 152斐波那契数列中每个数字是前两个数字之和,计算斐波那契数的过程可以很好地通过数据对象来表达任务依赖关系。使用FFRT并发编程框架计算斐波那契数的代码如下所示: 153 154```c 155#include <stdio.h> 156#include "ffrt/ffrt.h" 157 158typedef struct { 159 int x; 160 int* y; 161} fib_ffrt_s; 162 163void fib_ffrt(void* arg) 164{ 165 fib_ffrt_s* p = (fib_ffrt_s*)arg; 166 int x = p->x; 167 int* y = p->y; 168 169 if (x <= 1) { 170 *y = x; 171 } else { 172 int y1, y2; 173 fib_ffrt_s s1 = {x - 1, &y1}; 174 fib_ffrt_s s2 = {x - 2, &y2}; 175 176 // 构建数据依赖 177 ffrt_dependence_t dx_deps[] = {{ffrt_dependence_data, &x}}; 178 ffrt_deps_t dx = {1, dx_deps}; 179 ffrt_dependence_t dy1_deps[] = {{ffrt_dependence_data, &y1}}; 180 ffrt_deps_t dy1 = {1, dy1_deps}; 181 ffrt_dependence_t dy2_deps[] = {{ffrt_dependence_data, &y2}}; 182 ffrt_deps_t dy2 = {1, dy2_deps}; 183 ffrt_dependence_t dy12_deps[] = {{ffrt_dependence_data, &y1}, {ffrt_dependence_data, &y2}}; 184 ffrt_deps_t dy12 = {2, dy12_deps}; 185 186 // 分别提交任务 187 ffrt_submit_f(fib_ffrt, &s1, &dx, &dy1, NULL); 188 ffrt_submit_f(fib_ffrt, &s2, &dx, &dy2, NULL); 189 190 // 等待任务完成 191 ffrt_wait_deps(&dy12); 192 *y = y1 + y2; 193 } 194} 195 196int main() 197{ 198 int r; 199 fib_ffrt_s s = {5, &r}; 200 ffrt_dependence_t dr_deps[] = {{ffrt_dependence_data, &r}}; 201 ffrt_deps_t dr = {1, dr_deps}; 202 ffrt_submit_f(fib_ffrt, &s, NULL, &dr, NULL); 203 204 // 等待任务完成 205 ffrt_wait_deps(&dr); 206 printf("Fibonacci(5) is %d\n", r); 207 return 0; 208} 209``` 210 211预期输出为: 212 213```plain 214Fibonacci(5) is 5 215``` 216 217示例中将`fibonacci(x-1)`和`fibonacci(x-2)`作为两个任务提交给FFRT,在两个任务完成之后将结果进行累加。虽然单个任务只是拆分成两个子任务,但是子任务又可以继续进行拆分,因此整个计算图的并行度是非常高的。 218 219各个任务在FFRT内部形成了一颗调用树: 220 221 222 223## 接口说明 224 225上述样例中涉及到主要的FFRT的接口包括: 226 227| 名称 | 描述 | 228| ---------------------------------------------------------- | -------------------------------- | 229| [ffrt_submit_f](ffrt-api-guideline-c.md#ffrt_submit_f) | 提交任务调度执行。 | 230| [ffrt_submit_h_f](ffrt-api-guideline-c.md#ffrt_submit_h_f) | 提交任务调度执行并返回任务句柄。 | 231| [ffrt_wait_deps](ffrt-api-guideline-c.md#ffrt_wait_deps) | 等待依赖的任务完成。 | 232 233> **说明:** 234> 235> - 如何使用FFRT C++ API详见:[FFRT C++接口三方库使用指导](ffrt-development-guideline.md#using-ffrt-c-api-1)。 236> - 使用FFRT C接口或C++接口时,都可以通过FFRT C++接口三方库简化头文件包含,即使用`#include "ffrt/ffrt.h"`头文件包含语句。 237 238## 约束限制 239 240- 使用`ffrt_submit_base`接口进行任务提交时,每个任务的输入依赖和输出依赖的数量之和不能超过8个。 241- 使用`ffrt_submit_h_base`接口进行任务提交时,每个任务的输入依赖和输出依赖的数量之和不能超过7个。 242- 参数既作为输入依赖又作为输出依赖的时候,统计依赖数量时只统计一次,如输入依赖是`{&x}`,输出依赖也是`{&x}`,实际依赖的数量是1。 243