当C/C++后台开发遇上Coroutine

说来有点无厘头,Coroutine最近在公司的C/C++后台开发界,莫名其妙就火起来了,话说这货Melvin Conway在1963年的paper就已经提出来了,半个世纪过去了,咋突然冒出那么多粉丝出来,个人猜测与微信后台近期的Coroutine改造不无关系,也许这就是所谓的技术影响力吧,呵呵!

WHY?

首先,强烈推荐大家花点时间读一读《State Threads for Internet Applications》这篇文章,这是State Threads库的入门介绍,对于后台系统的performance、load scalability、system scalability等概念进行了非常精彩的论述。这里简单回顾一下常见的后台架构:Multi-Process Architecture(MP)、Multi-Threaded Architecture(MT)、Event-Driven State Machine Architecture(EDSM),这里,MP+EDSM、MT+EDSM等衍生品就不一一赘述了。

 Load ScalabilitySystem ScalabilityCode Readability
MPLowHighHigh
MTMediumMediumMedium
EDSMHighLowLow

这些年,随着硬件能力的不断提高,单机能力对于整个分布式系统的重要性不断下降,相反,对于开发者的开发效率、代码可读性等要求越来越高。这时,Coroutine就可以派上用场了,利用Coroutine的可重入特性,只要我们在底层库做适当的封装,上层的业务开发人员就可以像写同步程序那样写异步server了。

WHAT?

"Subroutines are special cases of ... coroutines." –Donald Knuth.

Wiki的定义:协程是一种程序组件,是由子例程(过程、函数、例程、方法、子程序)的概念泛化而来的,子例程只有一个入口点且只返回一次,而协程允许多个入口点,可以在指定位置挂起和恢复执行。

直观的定义:

  • 协程的本地数据在后续调用中始终保持
  • 协程在控制离开时暂停执行,当控制再次进入时只能从离开的位置继续执行
贴一个Wiki上的生产者-消费者Coroutine版,大家感受一下
var q := new queue  
  
coroutine produce  
    loop  
        while q is not full  
            create some new items  
            add the items to q  
        yield to consume  
  
coroutine consume  
    loop  
        while q is not empty  
            remove some items from q  
            use the items  
        yield to produce  
C/C++语言原生并不支持Coroutine,但是群众的力量是巨大的,正所谓“八仙过海各显神通”: setjmp and longjmpgetcontext, setcontext, makecontext and swapcontext,详细的清单可以参考 Implementations for CImplementations for C++
这里推荐一下云风的C语言版Coroutine实现: https://github.com/cloudwu/coroutine/,有人可能觉得这个实现过于简陋,但是我恰恰欣赏它的简单,不到200行的代码,没有任何多余的feature,非常适合新手入门。
typedef void (*coroutine_func)(struct schedule *, void *ud);  
  
struct schedule * coroutine_open(void);  
void coroutine_close(struct schedule *);  
  
int coroutine_new(struct schedule *, coroutine_func, void *ud);  
void coroutine_resume(struct schedule *, int id);  
int coroutine_status(struct schedule *, int id);  
int coroutine_running(struct schedule *);  
void coroutine_yield(struct schedule *);  
友情提醒:阅读代码时,留心_save_stack函数的实现,通过一点小技巧实现了Coroutine的栈区按需保存,对于内存的节省还是比较可观的。

HOW?

现在,假设我们有了Coroutine这个工具,又该如何整合进Server呢?为了解答这个问题,我们可以先问问自己,心目中的Server代码应该长啥样呢?

static int HandlerForCmdX(AsyncServer *server, const struct sockaddr_in &client_addr, const char *pkg, size_t len)
{
    // do some prepare stuff
    server->AsyncSendRecv(req_info_1, rsp_info_1);
    // do some prepare stuff
    server->AsyncSendRecv(req_info_2, rsp_info_2);
    // send response to client
    server->AsyncSendOnly(LISTEN_CHANNEL, client_addr, rsp_buf, rsp_len);
    return 0;
}

int main(int argc, char *argv[])
{
    AsyncServer server;	
    // do any init stuff
    server.AddCommand(USER_COMMAND_X, LISTEN_CHANNEL, HandlerForCmdX);
    // do any else stuff
    server.Run();
    exit(EXIT_SUCCESS);
}
上面的代码看上去挺美,再也不用纠结于各种状态跳转了,简单的注册命令处理函数HandlerForCmdX,然后如行云流水般添加业务处理逻辑,步骤1->步骤2->...->步骤N,完全符合人类的思维模式,从此PM再也不用担心我的开发效率了 偷笑。想法其实很简单:server所谓的阻塞操作,基本也就等同于网络IO(这里不讨论文件IO、sleep等,同理可以实现),只要封装出类似AsyncSendRecv这些接口,默默地替上层应用完成coroutine的切换,这样上层应用就可以用同步的方式写异步server了。
下面我以云风的coroutine库为例,简单介绍一下封装方法:一个请求进来之后,server会自动为它分配一个coroutine进行处理(与MP、MT的one-to-one模式类似),该coroutine会执行事先注册好的处理函数,比如前文的 HandlerForCmdX,执行到AsyncSendRecv内部时,一旦send成功,该coroutine就主动coroutine_yield出去,继续执行底层网络事件循环,当收到响应后,框架做一些简单的解包工作,得到其sequence号,通过一定的手段转换为相应的coroutine_id(coroutine_new的返回值),然后就可以通过coroutine_resume回到之前的AsyncSendRecv现场,紧接着控制权又回到了HandlerForCmdX,好像什么事都没有发生一样。"Talk is cheap. Show me the code."

int AsyncServer::SendOneReq(const ReqInfo &req_info)
{
    if (this->SetPeerAddr(req_info.channel_name, (struct sockaddr_in *)&req_info.peer_addr) != 0) return -1;
    if (this->SendOutData(req_info.data_buf, req_info.data_len, req_info.channel_name) != 0) return -2;
    co_map.insert(std::pair<uint32_t, int>(req_info.sequence, coroutine_running(co_schedule)));
    return 0;
}

int AsyncServer::RecvOneRsp(RspInfo &rsp_info)
{
    if (!decode_result) {
        rsp_info.timeout = true;
        return 0;
    }
    rsp_info.timeout = false;
    rsp_info.Resize(decode_result->m_uiPkgLen);
    memcpy(rsp_info.data_buf, decode_result->m_cpPkg, decode_result->m_uiPkgLen);
    return 0;
}

int AsyncServer::AsyncSendRecv(const ReqInfo &req_info, RspInfo &rsp_info)
{
    if (SendOneReq(req_info) != 0) return -1;
    coroutine_yield(co_schedule);
    if (RecvOneRsp(rsp_info) != 0) return -2;
    return 0;
}
其中,co_map用于保存sequence->coroutine_id的映射关系,当网络框架的事件循环接收到响应包后,解出sequence,从而得到coroutin_id,调用 coroutine_resume回到coroutine_yield处。这部分代码与网络框架息息相关,此处略过,有兴趣的读者可以单独交流。

Limitation

截止目前,一切看上去都挺美好的,似乎用上coroutine,后台开发就解放了,所以不得不提一些coroutine的局限性:

  • 虽然最终代码看起来和同步代码无异,而且单进程单线程,但用户注册的处理函数中不可以使用static变量(处理函数本身调用的函数不受限制)
  • 与EDSM类似,所有的coroutine隶属于同一个内核态线程,无法充分利用现代CPU的多核能力(可以考虑MP+Coroutine、MP+EDSM架构)
  • coroutine可以看做用户态线程,属于非抢占式,如果某个coroutine阻塞住了,整个server也就歇菜了(EDSM同样存在这个问题,有点吹毛求疵了)

致谢

最后,多谢skwang大牛的一路指点!

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值