纯C语言高性能异步多任务多事件驱动的协程库| Pure C language high-performance asynchronous multi-task multi-event driven coroutine library.
libatask(Asynchronous multitasking library)是一个由纯C语言编写的基于事件循环由多事件驱动的高性能协程库。它将事件与协程进行了结合,还原了协程最本质的用途。libatask性能高且资源占用极低,仅需56B的RAM,以及不到1K的CODE,不仅仅是PC环境,连最苛刻单片机环境中也能完美运行。
一个单向循环链表节点 | 当把事件封装成请求的时候,可以使用先进先出队列(libatask自带)创建一个请求队列,利用事件的链表节点可以很方便地将请求进行排队。同时libatask的事件循环队列也使用了该节点。因此,只有一个事件从其他的队列之中取出的时候才能被提交到事件循环之中。
事件的处理函数指针 | 当事件被处理时,可以调用事件的处理函数对事件进行处理。
事件处理函数的上下文指针 | 事件在被创建时可以指定事件携带的上下文,它将作为事件处理函数的第一个参数,libatask不对该字段进行任何处理。
事件的优先级 | 事件支持0-255的优先级,优先级高的事件进入事件循环之后,可以被优先处理。同等级的事件按先进先出顺序处理。
libatask提供了一个极轻量级的协程,该协程甚至不依赖于libatask库,只需要包含文件bp.h即可。 协程的使用方式如下:
定义一个uint8_t *bpd的一个指针变量,该指针指向用于保存协程断点位置的变量。如:uint8_t *bpd = &user_data->bp;
在协程函数开始的位置使用bpd_begin(N),结束的位置使用bpd_end(),一个函数只能有一个协程。 注:N是一个纯数字,从0到255,N表示了协程代码块中存在的断点个数。
使用bpd_yield(N) ret_val,对协程执行挂起操作(实际执行的是return ret_val操作)。N表示协程的第N个断点号(纯数字),同时,协程内的断点号必须从0到bpd_begin中的N连续。bpd_yield记录当前返回的位置,记录值N保存在bpd指针指向的变量中。协程函数在下次调用时将返回当前执行位置。
值得注意的是,协程函数的局部变量,在执行bpd_yield之后消失,因为bpd_yield最后调用的是return,因此,需要在yield之后使用的变量应该保存在协程函数外部,并通过参数传递进来。
用法示例:
uint8_t *bpd = &user->bp;
uint a;
bpd_begin(2);
/* code */
a = 100;
bpd_yield(1);
/* !!此时a的值已经消失 */
/* code */
bpd_yield(2);
bpd_end();
libatask在轻量级协程的基础上实现了一个在bpd_yield之后不丢失变量的方法。并且实现异步调用等功能。
libatask协程的函数原型为一个特殊的事件回调函数,即:void task_func(task_t *task, event_t *ev ...)
其中...表示用户自定义的参数,如下函数都是合法的协程函数。
void task1(task_t *task, event_t *ev, void *arg1, int arg2, float arg3);
void task1(task_t *task, event_t *ev, void *arg1, struct tm arg2, int arg3, float arg4);
使用task_init初始化一个task_t的协程任务,并指定协程任务的栈buffer和栈大小
创建协程函数原型如:void task_func(task_t *task, event_t *ev ...)
协程函数中使用TASK_BPD宏获取协程的断点变量。uint8_t *bpd = TASK_BPD(task);
从task的栈中分配当前协程需要在yield之后保存的异步变量。使用方式如下:
void task1(task_t *task, event_t *ev)
{
uint8_t *bpd = TASK_BPD(task);
struct vars
{
int a;
float b;
event_t c;
...
} *vars = (struct vars *)task_asyn_vars_get(task, sizeof(*vars));
bpd_begin(1);
bpd_yield(1);
/* 通过vars->可以获取保存在task栈中的变量 */
printf("%d %f\n", vars->a, vars->b);
bpd_end();
task_asyn_return(task);
}
在libatask的task_t中,存在一个task->event事件成员。task->event在协程运行中已经初始化完成(事件上下文为task,回调函数为当前协程)。因此,协程中的事件可以直接使用如:event_init_inherit(child_event, &task->event),对child_event进行继承初始化(将继承task->event中的回调,上下文,优先级)。当child_event事件触发后,将自动回调当前协程。
libatask的协程挂起后的恢复是由事件回调完成,并且支持多事件同时工作。示例如下:
void task1(task_t *task, event_t *ev, type1 arg1, type2 arg2)
{
uint8_t *bpd = TASK_BPD(task);
struct vars
{
type1 arg1;
type2 arg2;
timer_event_t timer;
io_event_t io_ev;
} *vars = (struct vars *)task_asyn_vars_get(task, sizeof(*vars));
bpd_begin(1);
/* 若需要在yield之后继续使用arg1、arg2则应该在
* bpd_begin与bpd_yield(1)之间将传入的参数保存
* 到异步变量之中 */
vars->arg1 = arg1;
vars->arg2 = arg2;
/* 事件使用继承的方式初始化,继承至协程的event */
timer_init_inherit(&vars->timer, &task->event);
event_init_inherit(&vars->io_ev.event, &task->event);
/* 同时开启了I/O事件与Timer事件 */
io_read(&vars->io_ev);
el_timer_start_ms(&vars->timer, 3000);
/* 协程挂起,并等待任意一个事件的到来 */
bpd_yield(1);
/* 判断到来的事件是哪一个事件 */
if (ev == &vars->timer)
{
/* 超时了,取消I/O操作 */
io_cancel(&vars->io_ev);
}
else
{
/* I/O正常到达,取消超时功能 */
el_timer_stop(&vars->timer);
}
bpd_end();
task_asyn_return(task);
}
libatask支持异步调用子协程,使用task_bpd_asyn_call(N, task, func, arg1, arg2, ...)进行调用,N为断点号。func为子协程函数,原型为void func(task_t *task, event_t *ev, type1 arg1, type2 arg2, ...)。
协程任务需要在bpd_end之后使用task_asyn_return返回父协程,当协程处于顶层协程(由task_start开启的协程)时task_asyn_return动作将结束任务并触发由task_end_wait等待的事件。
注:异步调用时必须确保当前协程没有正在等待的事件,否则等待的事件返回将破坏协程的栈。
示例
libatask自带了一个单向循环链表,该链表拥有以下特性:
FIFO与LIFO是基于slist实现的先进先出与后进先出的队列。LIFO相对于FIFO,只使用了sizeof(void *)的内存且效率更高,而FIFO使用了2 * sizeof(void *)。当元素没有先进先出的顺序要求时,使用LIFO更好。
libatask的事件包含了一个slist的链表节点,因此可以将事件作为fifo或lifo或者slist的元素进行队列操作。事件的节点可以使用EVENT_NODE(event)取得。
libatask自带了一个定时器功能,API如下:
libatask实现了一个基于事件的信号量功能,使用sem_init初始化一个信号量,使用sem_give于sem_take增加或减少信号量。信号量一般配合协程使用。
使用示例如下:
/* 这是协程等待任务开始信号的实现 */
while (user->state == STATE_STOP)
{
sem_take(&user->sem, &user->task.event);
bpd_yield(1);
}
/* 唤醒处 */
user->state = STATE_RUN;
sem_give(&user->sem, NULL);
假如协程未等待信号量,则使用sem_give(&user->sem, NULL)不做任何操作,若协程正在等待该信号量,则sem_give将唤醒协程。
libatask实现了一个块式无碎片的内存池分配功能,可基于事件实现内存不足时的等待功能。
API如下: