这两天,主要干的事情就是研究了一下协程。哈哈,由于我本身是搞嵌入式的,基本上很少碰到用这个东东的。看网上说的这么神奇,貌似服务器上经常用协程来大规模并发时间,所以我决定也把协程引入进Obotcha。PS:最新的C++20貌似也有协程了,但是我现在用的是C++14,呵呵。暂时还木有~。
协程库我用了libco,为啥用libco,主要是网上吹嘘说这个库是微信后台使用的,牛叉的很。
Libco 是微信后台大规模使用的 C++ 协程库,在 2013 年的时候作为腾讯六大开源项目首次开源。
据说 2013 年至今仍稳定地运行在微信后台的数万台机器上。从本届 ArchSummit 北京峰会来自腾
讯内部的分享经验来看,它在腾讯内部使用确实是比较广泛的。
好吧,我们先来看一下libco的代码。粗略搂了一眼,就发现一些很奇怪的地方:
问题1:
代码:co_routine.cpp
static __thread stCoRoutineEnv_t* gCoEnvPerThread = NULL;
gCoEnvPerThread是一个threadlocal变量,那就意味这gCoEnvPerThread在每个thread上都有一个备份。那就是说:如果我在A线程上启动了一个协程A1,B线程上启动一个协程B1,其实这2个协程是无法相互切换的。用于保存A1和A2的栈信息的gCoEnvPerThread是不一样的。换句话说,就是所有的协程必须跑在一个线程上,否则他们之间是无法切换的。这样的话,每次启动一个协程都需要判断一下自己和需要切换的协程是不是在同一个thread中,感觉很容易踩坑。
问题2
struct stCoRoutineEnv_t
{
stCoRoutine_t *pCallStack[ 128 ];
int iCallStackSize;
stCoEpoll_t *pEpoll;
//for copy stack log lastco and nextco
stCoRoutine_t* pending_co;
stCoRoutine_t* occupy_co;
};
.....
void co_resume( stCoRoutine_t *co )
{
stCoRoutineEnv_t *env = co->env;
stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ];
if( !co->cStart )
{
coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co,0 );
co->cStart = 1;
}
env->pCallStack[ env->iCallStackSize++ ] = co;
co_swap( lpCurrRoutine, co );
}
用来存放栈信息的callStack只有128个,也就是说我在一个thread中最多能co_create 128个协程。我看没有任何针对iCallStackSize的检测,如果co_resume启动129个协程序的话,超过128就直接crash了?呃,好神奇。难道每次co_resume都要自己去检查一下iCallStackSize的大小吗?
问题3
static void *readwrite_routine( void *arg )
{
co_enable_hook_sys();
....
}
看sample代码,你会发现,每个协程的处理函数入口都需要调用这个函数:co_enable_hook_sys。这个函数其实就是将每个协程栈的cEnableSysHook设定成1,这样每次调用系统函数(connect,socket等等)都会用到hook的函数,而不是libc的函数。好吧,这里就有个逻辑问题,所有的协程都在一个线程里面(问题一),如果有一个协程不用hook的函数,用原生的io函数,那不就还是block了整个线程的处理吗?所以其实只要是协程,它就必须用hook的io函数吧,也就是说co_enable_hook_sys感觉就像多余的一样。
问题四:
void co_eventloop( stCoEpoll_t *ctx,pfn_co_eventloop_t pfn,void *arg )
{
for(;;)
{
int ret = co_epoll_wait( ctx->iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, 1);
......
}
}
co_eventloop主要是为了监听io操作,所有的io处理都会通过这个co_epoll_wait来获取数据。好吧,关键在epoll的时间上,1ms~~~。为了没有io block,所以没1ms轮询一次,这样真的好吗?可能腾讯的服务器比较忙,所以这样的轮询性能比较好,但是,如果服务器不忙,或者只是间断性繁忙的情况,这样做是不是有点不脱?
针对问题一,目前我只能规定要想启动一个系列的协程序(例如socket收/发),必须先启动一个线程,然后在线程里面启动这些协程。代码如下:
//FilaCroutine实际上是一个线程,每次构造函数都传入一系列的协程序。
//但是不启动
_FilaCroutine::_FilaCroutine(ArrayList<Filament> filaments) {
this->mFilaments = filaments;
}
void _FilaCroutine::start() {
st(Thread)::start();
}
//FilaCroutine线程启动之后,再启动所有的协程,这样就能保证这些协程能相互切换了。
void _FilaCroutine::run() {
co_init_curr_thread_env();
co_enable_hook_sys();
ListIterator<Filament> iterator = mFilaments->getIterator();
while(iterator->hasValue()) {
printf("start run 1 \n");
Filament fila = iterator->getValue();
fila->start();
iterator->next();
}
co_eventloop( co_get_epoll_ct(),0,0 );
}
针对问题二:不知道是不是能改成vector?哈哈
针对问题三:在Obotcha中,只在线程启动的时候调用co_enable_hook_sys。将cEnableSysHook从协程的结构体移到了stCoRoutineEnv_t中,这样一个线程设定一次即可。参看上面的_FilaCroutine::run() 函数。
总结:
个人对libco有点小失望。哈哈,也可能是自己能力有限,上述问题没有考虑周全。不喜勿喷。针对libco的修改参见如下:
https://gitee.com/baimant/Obotcha/commit/8053b40538fd8b58218811ed6734c1da026ed011
谢谢。