对于一个新东西,应该怎么学?
1、如果是一个你没接触过的,我建议第一步,看下官方的说明,跑一下官方的demo
2、如果是一个你熟悉的,那就撸起袖子开始干,直接跳过demo。
我先以一个libco的简单demo example_cond.cpp,,来看看libco是怎么来用的
void* Producer(void* args)
{
co_enable_hook_sys();
stEnv_t* env= (stEnv_t*)args;
int id = 0;
while (true)
{
stTask_t* task = (stTask_t*)calloc(1, sizeof(stTask_t));
task->id = id++;
env->task_queue.push(task);
printf("%s:%d produce task %d\n", __func__, __LINE__, task->id);
co_cond_signal(env->cond);
poll(NULL, 0, 1000);
}
return NULL;
}
void* Consumer(void* args)
{
co_enable_hook_sys();
stEnv_t* env = (stEnv_t*)args;
while (true)
{
if (env->task_queue.empty())
{
co_cond_timedwait(env->cond, -1);
continue;
}
stTask_t* task = env->task_queue.front();
env->task_queue.pop();
printf("%s:%d consume task %d\n", __func__, __LINE__, task->id);
free(task);
}
return NULL;
}
int main()
{
stEnv_t* env = new stEnv_t;
env->cond = co_cond_alloc();
// 先创建了消费者
stCoRoutine_t* consumer_routine;
co_create(&consumer_routine, &attr, Consumer, env);
co_resume(consumer_routine);
// 再传建了生产者
stCoRoutine_t* producer_routine;
co_create(&producer_routine, &attr, Producer, env);
co_resume(producer_routine);
// libco的事件循环
co_eventloop(co_get_epoll_ct(), NULL, NULL);
return 0;
}
输出的结果:
可以看出,不需要启动任何线程,只需要主线程,就能实现一个协程发,一个协程收,这在没有协程的程序,是不是感觉应该做不到!
demo中用到了哪些函数
从上面的demo,我们可以看到,他用到了几个关键的函数,也就是这几个关键的函数,来为我们实现了整个协程的框架通信和切换
1、co_create :创建一个协程
2、co_resume :启动一个协程
3、co_eventloop :协程调度事件循环
4、poll:等待并让出cpu资源 co_cond_signal:协程信号量 co_cond_timedwait:等待信号量。
co_create实现
创建一个协程需要些什么东西:
接口:
int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg )
从入参我们可以看出,需要协程的属性attr,协程的回调函数pfn,以及协程的参数arg,这个跟线程的创建的参数很像。
先看看属性需要啥?
共享栈的指针,共享栈的大小,说明如果是设置共享栈,还需要外部把栈创建好传进来的。这里有个印象就行,栈大小是指保存的栈多大,默认128k。
其中__attribute__ ((packed)) 就是结构体的对齐方式使用1字节对齐,也就是不会根据结构体成员来选择对齐的方式
struct stCoRoutineAttr_t
{
int stack_size; // 栈大小
stShareStack_t* share_stack; // 共享栈指针
stCoRoutineAttr_t()
{
stack_size = 128 * 1024;
share_stack = NULL;
}
}__attribute__ ((packed)); // 一字节对齐
实现
if( !co_get_curr_thread_env() )
{
// 创建了主协程,创建了gCoEnvPerThread 线程全局环境变量,只运行一次
co_init_curr_thread_env();
}
// 创建子协程
stCoRoutine_t *co = co_create_env( co_get_curr_thread_env(), attr, pfn, arg );
*ppco = co;
return 0;
可以看出,创建子协程需要用到,子协程接口
struct stCoRoutine_t *co_create_env( stCoRoutineEnv_t * env, const stCoRoutineAttr_t* attr,pfn_co_routine_t pfn,void *arg )
其中需要的结构:
struct stCoRoutineEnv_t
{
stCoRoutine_t *pCallStack[ 128 ]; // 调用栈,第0个是主协程
int iCallStackSize; // 主协程+1 resume +1 yield -1
stCoEpoll_t *pEpoll;
//for copy stack log lastco and nextco // 为共享栈准备的,独立栈为空
stCoRoutine_t* pending_co;
stCoRoutine_t* occupy_co;
};
其中,有一个调用栈的数组,pCallStack,这个的0下标应该是主协程
而在创建子协程时,这个结构还没有初始化,所以创建子协程之前,需要co_init_curr_thread_env(),再来看看他的实现
// 线程的全局变量,每个线程独立,是基于单一线程全局的变量,因为没有该关键字,就变成了进程的全局变量
static __thread stCoRoutineEnv_t* gCoEnvPerThread = NULL;
void co_init_curr_thread_env()
{
gCoEnvPerThread = (stCoRoutineEnv_t*)calloc( 1, sizeof(stCoRoutineEnv_t) );
stCoRoutineEnv_t *env = gCoEnvPerThread;
// 这是创建一个主协程
struct stCoRoutine_t *self = co_create_env( env, NULL, NULL,NULL );
self->cIsMain = 1;
// 清空coctx,主协程没有,为啥不完全在co_create_env里面初始化,因为主协程的特殊性
coctx_init( &self->ctx );
// 这些无用功,上面已经calloc了
env->iCallStackSize = 0;
env->pending_co = NULL;
env->occupy_co = NULL;
// 把全局的env的第一个调用栈给了主协程
env->pCallStack[env->iCallStackSize++] = self;
// epoll 初始化
stCoEpoll_t *ev = AllocEpoll();
SetEpoll( env, ev );
}
主要的代码注释都写了,基本上应该没什么阅读上的难度。其中有一个变量gCoEnvPerThread,是一个__thread 类型,这是一个线程的相关的局部变量,每个线程不同,协程共享。
再看看真正的创建子协程的实现
struct stCoRoutine_t *co_create_env( stCoRoutineEnv_t * env, const stCoRoutineAttr_t* attr,
pfn_co_routine_t pfn,void *arg )
{
stCoRoutineAttr_t at;
if( attr )
{
memcpy( &at, attr, sizeof(at));
}
// 1. 获取要分配的存放栈的堆大小
if( at.stack_size <= 0 ) // 默认128K
{
at.stack_size = 128 * 1024;
}
else if( at.stack_size > 1024 * 1024 * 8 ) // >8M 设置8M
{
at.stack_size = 1024 * 1024 * 8;
}
if( at.stack_size & 0xFFF ) // 大于4K的,不对齐,则4K对齐,内存的页是4k对齐,这样可以减少内存碎片
{
at.stack_size &= ~0xFFF;
at.stack_size += 0x1000;
}
// 2.如果外部应用co_alloc_sharestack创建了共享栈,则从共享栈直接获取,否则根据stack_size分配
stStackMem_t* stack_mem = NULL;
if (at.share_stack)
{
stack_mem = co_get_stackmem(at.share_stack);
at.stack_size = at.share_stack->stack_size;
}
else
{
stack_mem = co_alloc_stackmem(at.stack_size);
}
// 3.创建协程对象并初始化,这里的顺序被我调了
stCoRoutine_t *lp = (stCoRoutine_t*)malloc( sizeof(stCoRoutine_t) );
memset( lp, 0, (long)(sizeof(stCoRoutine_t)));
lp->env = env;
lp->pfn = pfn;
lp->arg = arg;
lp->stack_mem = stack_mem;
lp->ctx.ss_sp = stack_mem->stack_buffer;
lp->ctx.ss_size = at.stack_size;
lp->cStart = 0;
lp->cEnd = 0;
lp->cIsMain = 0;
lp->cEnableSysHook = 0;
lp->cIsShareStack = at.share_stack != NULL;
lp->save_size = 0;
lp->save_buffer = NULL;
return lp;
}
这里我调整了官方的顺序,效果完全一样,方便大家阅读,其中分为三步,每一步对变量进行初始化。lp是真正创建出来的协程实体,其各个变量定义如下
struct stCoRoutine_t
{
stCoRoutineEnv_t *env; // 线程独有的参数
pfn_co_routine_t pfn; // 协程函数
void *arg; // 协程参数
coctx_t ctx; // 保存协程cpu上下文
char cStart; // 启动
char cEnd; // 结束让出
char cIsMain; // 标记是否是主协程
char cEnableSysHook; // 是否使能hook
char cIsShareStack; // 是否是共享栈
void *pvEnv; // 系统环境变量的键值对
//char sRunStack[ 1024 * 128 ];
stStackMem_t* stack_mem; // 本协程对应的栈内存
//save satck buffer while confilct on same stack_buffer;
//如果切入和切出的协程不同,则需要保存
char* stack_sp; // 作为保存共享栈栈顶的指针
unsigned int save_size; // 共享栈buffer的大小
char* save_buffer; // 共享栈栈底的堆的初始位置
stCoSpec_t aSpec[1024];
};
到这里,协程的创建完成。协程的启动在下一个文章分享