libco 共享栈测试分析与实现
5. 共享栈模式
这种做法有什么好处?其实我们可以直接想想以前的方法(每个协程单独分配栈)有什么坏处好了:
-
以前的方法为每个协程都单独分配一段内存空间,因为是固定大小的,实际使用中协程并不能使用到这么大的内存空间,于是就会造成非常大的内存浪费(有同学一定会问为什么不用
Split Stack
,这个东西的性能有多垃圾有目共睹)。而且因为绝大多数协程使用的栈空间都极少,复制栈空间的开销非常小。 -
因为协程的调度是非抢占的(non-preempt),而在 libco 中,切换的时机都是做 I/O 的时候,并且只有在切换的时候才会去复制栈空间,所以开销也可控。
具体原理:我们一步步来看其调用,从其中明白他的原理
-
在协程环境初始化时,要先调用 (
co_alloc_sharestack
) 来分配共享栈的内容,其中第一个参数 count 是指分配多少个共享栈,stack_size 是指每个栈的大小 ,分配出来的结构名是stShareStack_t
。stShareStack_t
结构struct stShareStack_t { unsigned int alloc_idx; int stack_size; int count; stStackMem_t **stack_array; };
co_alloc_sharestack
//创建 count 个共享栈,大小为 stack_size stShareStack_t* co_alloc_sharestack(int count, int stack_size) { stShareStack_t* share_stack = (stShareStack_t*)malloc(sizeof(stShareStack_t)); share_stack->alloc_idx = 0;//初始化起始的分配游标 share_stack->stack_size = stack_size; //alloc stack array share_stack->count = count; //初始化栈空间 stStackMem_t** stack_array = (stStackMem_t**)calloc(count, sizeof(stStackMem_t*)); for (int i = 0; i < count; i++) { stack_array[i] = co_alloc_stackmem(stack_size); } share_stack->stack_array = stack_array; return share_stack; }
-
共享栈的结构是一个数组,它里面有
count
个元素,每个元素都是一个指向一段内存的指针stStackMem_t
。在新分配协程时(co_create_env)
,它会从刚刚分配的stShareStack_t
中,按RoundRobin
的方式取一个stStackMem_t
出来,然后就算作是这个协程自己的栈。显然,这个时候这个空间是与其它协程共享的,因此叫「共享栈」。
libco 源代码:example_copystack.cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>
#include "coctx.h"
#include "co_routine.h"
#include "co_routine_inner.h"
void *RoutineFunc(void *args)
{
co_enable_hook_sys();
int *routineid = (int *)args;
while (true)
{
char sBuff[128];
sprintf(sBuff, "from routineid %d stack addr %p\n", *routineid, sBuff);
printf("%s", sBuff);
poll(NULL, 0, 1000); //sleep 1s
}
return NULL;
}
int main()
{
stShareStack_t *share_stack = co_alloc_sharestack(1, 1024 * 128);
stCoRoutineAttr_t attr;
attr.stack_size = 0;
attr.share_stack = share_stack;
stCoRoutine_t *co[2];
int routineid[2];
for (int i = 0; i < 2; i++)
{
routineid[i] = i;
co_create(&co[i], &attr, RoutineFunc, routineid + i);
co_resume(co[i]);
}
co_eventloop(co_get_epoll_ct(), NULL, NULL);
return 0;
}
运行结果:
以上代码运行结果等同于下面:
/*
* Tencent is pleased to support the open source community by making Libco available.
* Copyright (C) 2014 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>
#include "coctx.h"
#include "co_routine.h"
#include "co_routine_inner.h"
void *RoutineFunc(void *args)
{
// co_enable_hook_sys();
int *routineid = (int *)args;
while (true)
{
char sBuff[128];
sprintf(sBuff, "from routineid %d stack addr %p\n", *routineid, sBuff);
printf("%s", sBuff);
// poll(NULL, 0, 1000); //sleep 1s
// sleep(1);
co_yield();
}
return NULL;
}
int main()
{
stShareStack_t *share_stack = co_alloc_sharestack(1, 1024 * 128);
stCoRoutineAttr_t attr;
attr.stack_size = 0;
attr.share_stack = share_stack;
stCoRoutine_t *co[2];
int routineid[2];
for (int i = 0; i < 2; i++)
{
routineid[i] = i;
co_create(&co[i], &attr, RoutineFunc, routineid + i);
}
// co_eventloop(co_get_epoll_ct(), NULL, NULL);
while (true)
{
co_resume(co[0]);
co_resume(co[1]);
}
return 0;
}
分析:
首先通过co_alloc_sharestack(1, 1024 * 128);
分配一个1024*128
的共享栈空间,然后将要创建的协程的参数设置为使用这块共享栈空间,之后创建并调用,eventloop
先不用管,hook层
主要实现了在遇到阻塞IO时自动切换协程,(如何阻塞由事件循环co_eventloop
检测的)阻塞IO完成时恢复协程,简化异步回调为相对同步方式的功能.那么这样看来就是在sleep
的时候,程序返回到主协程执行for
循环,当调用到第二个协程执行的时候,他也要使用这个共享栈,所以内部就是将第一个子协程的使用到的数据copy
到他自己的栈里去,然后把共享栈拿来给第二个使用即可.依次类推!!!
类比去看:云风协程库保存和恢复协程运行栈原理讲解
下面摘自好朋友宝彤大佬,我觉得说的很有道理^-^
一块share stack上的一个栈由多个协程共享,当一个协程要使用stack时,上一个协程要让出来(将栈内有效数据保存到自己的控制字内),然后新协程使用共享栈空间直到其他公用这块栈的协程要使用到他,否则它就一直占用这块栈空间(不管它是否在运行)
我的实现:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>
#include "coctx.h"
#include "routine.h"
#include "routine.cpp"
using namespace Tattoo;
Routine_t *co[2];
void *RoutineFunc(void *args)
{
// co_enable_hook_sys();
int *routineid = (int *)args;
while (true)
{
char sBuff[128];
sprintf(sBuff, "from routineid %d stack addr %p\n", *routineid, sBuff);
printf("%s", sBuff);
// poll(NULL, 0, 1000); //sleep 1s
// sleep(1);
co[*routineid]->Yield();
}
return NULL;
}
int main()
{
ShareStack_t *share_stack = new ShareStack_t(1, 1024 * 128);
// ShareStack_t *share_stack = co_alloc_sharestack(1, 1024 * 128);
RoutineAttr_t attr(0, share_stack);
int routineid[2];
for (int i = 0; i < 2; i++)
{
routineid[i] = i;
co[i] = new Routine_t(get_curr_thread_env(), &attr, RoutineFunc, routineid + i);
}
// co_eventloop(co_get_epoll_ct(), NULL, NULL);
while (true)
{
co[1]->Resume();
sleep(1);
co[0]->Resume();
}
return 0;
}
运行结果:
协程基本上就最最最基础的就算完成了,下来的计划就是 eventloop(参考muduo) -> conditional_variable ->内存泄露 -> hook层等等
代码地址:MyLibCo
求 star ,fork