场景介绍
Function Flow编程模型是一种基于任务和数据驱动的并发编程模型,允许开发者通过任务及其依赖关系描述的方式进行应用开发。FFRT(Function Flow运行时)是支持Function Flow编程模型的软件运行时库,用于调度执行开发者基于Function Flow编程模型开发的应用。通过Function Flow编程模型和FFRT,开发者可专注于应用功能开发,由FFRT在运行时根据任务依赖状态和可用执行资源自动并发调度和执行任务。
本文用于指导开发者基于Function Flow编程模型和FFRT实现并行编程。
两种编程模型
线程编程模型 | FFRT任务编程模型 | |
---|---|---|
并行度挖掘方式 | 程序员通过创建多线程并把任务分配到每个线程中执行来挖掘运行时的并行度。 | 程序员(编译器工具或语言特性配合)静态编程时将应用分解成任务及其数据依赖关系,运行时调度器分配任务到工作线程执行。 |
谁负责线程创建 | 程序员负责创建线程,线程编程模型无法约束线程的创建,滥用可能造成系统中大量线程。 | 由调度器负责工作线程池的创建和管理,程序员无法直接创建线程。 |
负载均衡 | 程序员静态编程时将任务映射到线程,映射不合理或任务执行时间不确定造成线程负载不均。 | FFRT运行时根据线程执行状态调度就绪任务到空闲线程执行,减轻了线程负载不均问题。 |
调度开销 | 线程调度由内核态调度器完成,调度开销大。 | FFRT运行时在用户态以协程方式调度执行,相比内核线程调度机制更为轻量,减小调度的开销,并可通过硬化调度卸载进一步减小调度开销。 |
依赖表达 | 线程创建时即处于可执行状态,执行时与其他线程同步操作,增加线程切换。 | FFRT运行时根据任务创建时显式表达的输入依赖和输出依赖关系判断任务可执行状态,当输入依赖不满足时,任务不被调度执行。 |
基本概念
Function Flow 任务编程模型
Function Flow编程模型允许开发者通过任务及其依赖关系描述的方式进行应用开发,其主要特性包括Task-Based
和 Data-Driven
。
Task-Based 特性
Task-Based
指在Function Flow编程模型中开发者以任务方式来组织应用程序表达,运行时以任务粒度执行调度。
任务定义为一种面向开发者的编程线索和面向运行时的执行对象,通常包含一组指令序列及其操作的数据上下文环境。
Function Flow编程模型中的任务包含以下主要特征:
- 任务之间可指定依赖关系,依赖关系通过
Data-Driven
方式表达。 - 任务可支持嵌套,即任务在执行过程中可生成新的任务下发给运行时,形成父子任务关系。
- 多任务支持互同步操作,例如等待、锁、条件变量等。
注意
任务颗粒度影响应用执行性能,颗粒度过小增加调度开销,颗粒度过大降低并行度。Function Flow编程模型中任务的目标颗粒度最小为100us量级,开发者应注意合理控制任务颗粒度。
Data-Driven 特性
Data-Driven
指任务之间的依赖关系通过数据依赖表达。
任务执行过程中对其关联的数据对象进行读写操作。在Function Flow编程模型中,数据对象表达抽象为数据签名,每个数据签名唯一对应一个数据对象。
数据依赖抽象为任务所操作的数据对象的数据签名列表,包括输入数据依赖in_deps
和输出数据依赖out_deps
。数据对象的签名出现在一个任务的in_deps
中时,该任务称为数据对象的消费者任务,消费者任务执行不改变其输入数据对象的内容;数据对象的签名出现在任务的out_deps
中时,该任务称为数据对象的生产者任务,生产者任务执行改变其输出数据对象的内容,从而生成该数据对象的一个新的版本。
一个数据对象可能存在多个版本,每个版本对应一个生产者任务和零个,一个或多个消费者任务,根据生产者任务和消费者任务的下发顺序定义数据对象的多个版本的顺序以及每个版本所对应的生产者和消费者任务。
数据依赖解除的任务进入就绪状态允许被调度执行,依赖解除状态指任务所有输入数据对象版本的生产者任务执行完成,且所有输出数据对象版本的所有消费者任务执行完成的状态。
通过上述Data-Driven
的数据依赖表达,FFRT在运行时可动态构建任务之间的基于生产者/消费者的数据依赖关系并遵循任务数据依赖状态执行调度,包括:
-
Producer-Consumer 依赖
一个数据对象版本的生产者任务和该数据对象版本的消费者任务之间形成的依赖关系,也称为Read-after-Write依赖。
-
Consumer-Producer 依赖
一个数据对象版本的消费者任务和该数据对象的下一个版本的生产者任务之间形成的依赖关系,也称为Write-after-Read依赖。
-
Producer-Producer 依赖
一个数据对象版本的生产者任务和该数据对象的下一个版本的生产者任务之间形成的依赖关系,也称为Write-after-Write依赖。
例如,如果有这么一些任务,与数据A的关系表述为:
task1(OUT A);
task2(IN A);
task3(IN A);
task4(OUT A);
task5(OUT A);
为表述方便,本文中的数据流图均以圆圈表示 Task,方块表示数据。
可以得出以下结论:
- task1 与task2/task3 构成Producer-Consumer 依赖,即:task2/task3 需要等到task1 写完A之后才能读A。
- task2/task3 与task4 构成Consumer-Producer 依赖,即:task4 需要等到task2/task3 读完A之后才能写A。
- task4 与task5 构成Producer-Producer 依赖,即:task5 需要等到task4 写完A之后才能写A。
接口说明
接口名 | 描述 |
---|---|
ffrt_condattr_init (ffrt_condattr_t* attr) | 初始化条件变量属性。 |
ffrt_condattr_destroy(ffrt_condattr_t* attr) | 销毁条件变量属性。 |
ffrt_condattr_setclock(ffrt_condattr_t* attr, ffrt_clockid_t clock) | 设置条件变量的时钟属性。 |
ffrt_condattr_getclock(const ffrt_condattr_t* attr, ffrt_clockid_t* clock) | 获取条件变量的时钟属性。 |
ffrt_cond_init(ffrt_cond_t* cond, const ffrt_condattr_t* attr) | 初始化条件变量。 |
ffrt_cond_signal(ffrt_cond_t* cond) | 唤醒阻塞在条件变量上的一个任务。 |
ffrt_cond_broadcast(ffrt_cond_t* cond) | 唤醒阻塞在条件变量上的所有任务。 |
ffrt_cond_wait(ffrt_cond_t* cond, ffrt_mutex_t* mutex) | 条件变量等待函数,条件变量不满足时阻塞当前任务。 |
ffrt_cond_timedwait(ffrt_cond_t* cond, ffrt_mutex_t* mutex, const struct timespec* time_point) | 条件变量超时等待函数,条件变量不满足时阻塞当前任务,超时等待返回。 |
ffrt_cond_destroy(ffrt_cond_t* cond) | 销毁条件变量。 |
ffrt_mutex_init(ffrt_mutex_t* mutex, const ffrt_mutexattr_t* attr) | 初始化mutex。 |
ffrt_mutex_lock(ffrt_mutex_t* mutex) | 获取mutex。 |
ffrt_mutex_unlock(ffrt_mutex_t* mutex) | 释放mutex。 |
ffrt_mutex_trylock(ffrt_mutex_t* mutex) | 尝试获取mutex。 |
ffrt_mutex_destroy(ffrt_mutex_t* mutex) | 销毁mutex。 |
ffrt_queue_attr_init(ffrt_queue_attr_t* attr) | 初始化串行队列属性。 |
ffrt_queue_attr_destroy(ffrt_queue_attr_t* attr) | 销毁串行队列属性。 |
ffrt_queue_attr_set_qos(ffrt_queue_attr_t* attr, ffrt_qos_t qos) | 设置串行队列qos属性。 |
ffrt_queue_attr_get_qos(const ffrt_queue_attr_t* attr) | 获取串行队列qos属性。 |
ffrt_queue_create(ffrt_queue_type_t type, const char* name, const ffrt_queue_attr_t* attr) | 创建队列。 |
ffrt_queue_destroy(ffrt_queue_t queue) | 销毁队列。 |
ffrt_queue_submit(ffrt_queue_t queue, ffrt_function_header_t* f, const ffrt_task_attr_t* attr) | 提交一个任务到队列中调度执行。 |
ffrt_queue_submit_h(ffrt_queue_t queue, ffrt_function_header_t* f, const ffrt_task_attr_t* attr) | 提交一个任务到队列中调度执行,并返回任务句柄。 |
ffrt_queue_wait(ffrt_task_handle_t handle) | 等待队列中一个任务执行完成。 |
ffrt_queue_cancel(ffrt_task_handle_t handle) | 取消队列中一个任务。 |
ffrt_usleep(uint64_t usec) | 延迟usec微秒。 |
ffrt_yield(void) | 当前任务主动放权,让其他任务有机会调度执行。 |
ffrt_task_attr_init(ffrt_task_attr_t* attr) | 初始化任务属性。 |
ffrt_task_attr_set_name(ffrt_task_attr_t* attr, const char* name) | 设置任务名字。 |
ffrt_task_attr_get_name(const ffrt_task_attr_t* attr) | 获取任务名字。 |
ffrt_task_attr_destroy(ffrt_task_attr_t* attr) | 销毁任务属性。 |
ffrt_task_attr_set_qos(ffrt_task_attr_t* attr, ffrt_qos_t qos) | 设置任务qos。 |
ffrt_task_attr_get_qos(const ffrt_task_attr_t* attr) | 获取任务qos。 |
ffrt_task_attr_set_delay(ffrt_task_attr_t* attr, uint64_t delay_us) | 设置任务延迟时间。 |
ffrt_task_attr_get_delay(const ffrt_task_attr_t* attr) | 获取任务延迟时间。 |
ffrt_this_task_update_qos(ffrt_qos_t qos) | 更新任务qos。 |
ffrt_this_task_get_id(void) | 获取任务id。 |
ffrt_alloc_auto_managed_function_storage_base(ffrt_function_kind_t kind) | 申请函数执行结构的内存。 |
ffrt_submit_base(ffrt_function_header_t* f, const ffrt_deps_t* in_deps, const ffrt_deps_t* out_deps, const ffrt_task_attr_t* attr) | 提交任务调度执行。 |
ffrt_submit_h_base(ffrt_function_header_t* f, const ffrt_deps_t* in_deps, const ffrt_deps_t* out_deps, const ffrt_task_attr_t* attr) | 提交任务调度执行并返回任务句柄。 |
ffrt_task_handle_destroy(ffrt_task_handle_t handle) | 销毁任务句柄。 |
ffrt_skip(ffrt_task_handle_t handle) | 跳过指定任务。 |
ffrt_wait_deps(const ffrt_deps_t* deps) | 等待依赖的任务完成,当前任务开始执行。 |
函数介绍
任务管理
ffrt_submit_base
- 该接口为ffrt动态库的导出接口,基于此可以封装出C API ffrt_submit,满足二进制兼容。
声明
const int ffrt_auto_managed_function_storage_size = 64 + sizeof(ffrt_function_header_t);
typedef enum {
ffrt_function_kind_general,
ffrt_function_kind_queue
} ffrt_function_kind_t;
void* ffrt_alloc_auto_managed_function_storage_base(ffrt_function_kind_t kind);
typedef void(*ffrt_function_t)(void*);
typedef struct {
ffrt_function_t exec;
ffrt_function_t destroy;
uint64_t reserve[2];
} ffrt_function_header_t;
void ffrt_submit_base(ffrt_function_header_t* func, const ffrt_deps_t* in_deps, const ffrt_deps_t* out_deps, const ffrt_task_attr_t* attr);
参数
kind
- function子类型,用于优化内部数据结构,默认使用ffrt_function_kind_general类型。
func
- CPU Function的指针,该指针执行的数据结构,按照
ffrt_function_header_t
定义的描述了该CPU Task如何执行和销毁的函数指针,FFRT通过这两个函数指针完成Task的执行和销毁。
in_deps
- 该参数是可选的。
- 该参数用于描述该任务的输入依赖,FFRT 通过数据的虚拟地址作为数据的Signature 来建立依赖。
out_deps
- 该参数是可选的。
- 该参数用于描述该任务的输出依赖。
注意
:该依赖值本质上是一个数值,ffrt没办法区分该值是合理的还是不合理的,会假定输入的值是合理的进行处理;但不建议采用NULL,1, 2 等值来建立依赖关系,建议采用实际的内存地址,因为前者使用不当会建立起不必要的依赖,影响并发。
attr
- 该参数是可选的。
- 该参数用于描述Task 的属性,比如qos 等
返回值
- 不涉及。
描述
- 建议用户对ffrt_submit_base进行封装后调用,具体可参考样例。
- ffrt_submit_base作为底层能力,使用时需要满足以下限制:
- ffrt_submit_base入参中的func指针只能通过ffrt_alloc_auto_managed_function_storage_base申请,且二者的调用需一一对应。
- ffrt_alloc_auto_managed_function_storage_base申请的内存为ffrt_auto_managed_function_storage_size字节,其生命周期归ffrt管理,在该task结束时,由FFRT自动释放,用户无需释放。
- ffrt_function_header_t 中定义了两个函数指针:
- exec:用于描述该Task如何被执行,当FFRT需要执行该Task时由FFRT调用。
- destroy:用于描述该Task如何被销毁,当FFRT需要销毁该Task时由FFRT调用。
样例
template<class T>
struct function {
template<class CT>
function(ffrt_function_header_t h, CT&& c) : header(h), closure(std::forward<CT>(c)) {}
ffrt_function_header_t header;
T closure;
};
template<class T>
void exec_function_wrapper(void* t)
{
auto f = (function<std::decay_t<T>>*)t;
f->closure();
}
template<class T>
void destroy_function_wrapper(void* t)
{
auto f = (function<std::decay_t<T>>*)t;
f->closure = nullptr;
}
template<class T>
inline ffrt_function_header_t* create_function_wrapper(T&& func)
{
using function_type = function<std::decay_t<T>>;
static_assert(sizeof(function_type) <= ffrt_auto_managed_function_storage_size,
"size of function must be less than ffrt_auto_managed_function_storage_size");
auto p = ffrt_alloc_auto_managed_function_storage_base(ffrt_function_kind_general);