C/C++ 协程库boost.coroutine2、魅族libgo、腾讯libco、开源libaco详解

       最近研究高性能C++协程,网上了解到了魅族libgo、腾讯libco、开源libaco、boost coroutine,这里记录一下。

1 什么是协程

       协程可以很轻量的在子例程中进行切换,它由程序员进行子例程的调度(即切换)而不像线程那样需要内核参与,同时也省去了内核线程切换的开销,因为一个协程切换保留的就是函数调用栈和当前指令的寄存器,而线程切换需要陷入内核态,改变线程对象状态。

       go语言就已经把协程作为基础设施提供语言级的支持,cpp这种出了名的给程序员自由的语言肯定不会提供语言级的支持。
 

     各大开源协程库如:libgo, libtask, libmill, boost, libco等,他们都属于stackfull协程,每个协程有完整的私有堆栈,里面的核心就是上下文切换(context),而stackless的协程,比较出名的有protothreads,这个比较另类。

    那么现有协程库,是怎么去实现context切换的呢,目前主要有以下几种方式:

  1. 使用ucontext系列接口,例如:libtask
  2. 使用setjmp/longjmp接口,例如:libmill
  3. 使用boost.context,纯汇编实现,内部实现机制跟ucontext完全不同,效率非常高,后面会细讲,tbox最后也是基于此实现
  4. 使用windows的GetThreadContext/SetThreadContext接口
  5. 使用windows的CreateFiber/ConvertThreadToFiber/SwitchToFiber接口

各个协程协程库的切换效率的基准测试,可以参考:切换效率基准测试报告

 

2 协程库的设计与实现

个人认为,C++协程库从实现完善程度上分为以下几个层次


2.1.API级

    实现协程上下文切换api,或添加一些便于使用的封装; 特点:没有协程调度

    代表作:boost.context, boost.coroutine, ucontext(unix), fiber(windows)

    这一层次的协程库,仅仅提供了一个底层api,要想拿来做项目,还有非常非常遥远的距离;不过这些协程api可以为我们实现自己的协程库提供一个良好的基础。

2.2.玩具级

    实现了协程调度,无需用户手动处理协程上下文切换;特点:没有HOOK

代表作:libmill

    这一层次的协程库,实现了协程调度(类似于操作系统有了进程调度机制);稍好一些的意识到了阻塞网络io与协程的不协调之处,自己实现了一套网络io相关函数;

    但是这也意味着涉及网络的第三方库全部不可用了,比如你想用redis?不好意思,hiredis不能用了,要自己轮一个;你想用mysql?不好意思,mysqlclient不能用了,要自己轮一个。放弃整个C/C++生态全部自己轮,这个玩笑开的有点大,所以只能称之为“玩具级”。

2.3.工业级

    以部分正确的方式HOOK了网络io相关的syscall,可以少改甚至不改代码的兼容大多数第三方库;特点:没有完整生态

    代表作:libco

    这一层次的协程库,但是hook的不够完善,未能完全模拟syscall的行为,只能兼容行为符合预想的同步模型的第三方库,这虽然只能覆盖一部分的第三方库,但是通过严苛的源码审查、付出代价高昂的测试成本,也可以勉强用于实际项目开发了;

    但其他机制不够完善:协程间通讯、协程同步、调试等,因此对开发人员的要求很高,深谙底层机制才能写出没有问题的代码;再加上hook不完善带来的隐患,开发过程可谓是步步惊心、如履薄冰。

2.4.框架级

    以100%行为模拟的方式HOOK了网络io相关的syscall,可以完全不改代码兼容大多数第三方库;依照专为协程而生的语言的使用经验,提供了协程开发所必须的完整生态;

    代表作:libgo

    这一层次的协程库,能够100%模拟被hook的syscall的行为,能够兼容任何网络io行为的同步模型的第三方库;由于协程开发生态的完善,对开发人员的要求变得很低,新手也可以写出高效稳定的代码。但由于C++的灵活性,用户行为是不受限的,所以依然存在几个边边角角的难点需要开发者注意:没有gc(开发者要了解协程的调度时机和生命期),TLS的问题,用户不按套路出牌、把逻辑代码run在协程之外,粗粒度的线程锁等等。

5.语言级

    语言级的协程实现

    代表作:golang语言

    这一层次的协程库,开发者的一切行为都是受限行为,可以实现无死角的完善的协程。

 

3 协程库介绍

3.1 boost.coroutine2

       通过准标准库boost coroutine2库(boost coroutine已经废弃,建议使用boost coroutine2)为cpp提供的协程支持。

     从 1.54.0 版本开始,Boost.Asio 开始支持协程。异步编程是复杂的,协程可以让我们以同步的方式编写出异步的代码,在提高代码可读性的同时又不会丢失性能。
  在 Boost.Asio 要怎样才能使用协程呢?可以使用boost::asio::spawn()开启一个协程:

boost::asio::spawn(strand, echo);

void echo(boost::asio::yield_context yield) // 协程
{
    // ...
}

  spawn()的第一个参数可以是io_service,也可以是strand(如果需要在多线程中保证同步,就需要使用strand

Corountine2

     Corountine2是相对于Corountine而言的,在Boost v1.59被引入,Boost.Corountine目前已被标记为deprecated,因此不再提及。

    Boost.Corountine2使用了Boost.Context,因此要使用Boost.Corountine2,必须先编译Boost.Context。

    Boost.Corountine2几个特征:

  1. 非对称转移控制(放弃了对称转移)
  2. stackful
  3. 对象只能移动(moveable),不能拷贝(copyable),因为协程对象控制的有些资源,如栈,只能独享
  4. coroutine<>::push_type 和coroutine<>::pull_type保存栈使用的是块式内存,可动态扩展,因此不用关心初始栈的大小,在析构时,所有的内存都被释放。
  5. 上下文切换通过coroutine<>::push_type::operator() ,coroutine<>::pull_type::operator()来完成,因此这两个函数内部不能再调用自身。

      协程分为对称协程(symmetric)和非对称协程(asymmetric),对称协程需要显式指定将控制权yeild给谁,非对称协程可以隐式的转移控制权给它的调用者,boost coroutine2实现的是非对称协程.
      Boost库中的协程支持两种方式:一种是封装了Boost.Coroutine的spawn,是一个stackful类型的协程;一种是asio作者写出的stackless协程。

 

最简单的协程可以这样:

coroutines2.cpp

#include <boost/coroutine2/all.hpp>
#include <iostream>
int main() {
    boost::coroutines2::coroutine<void>::push_type coro(
        [&](boost::coroutines2::coroutine<void>::pull_type& yield) {
        std::cout << "hello world\n";
        yield();
    });
    coro();   //
    return 0;
}

编译:

g++ -std=c++11 -I. -I/home1/irteam/externals/boost/include -L/home1/irteam/externals/boost/lib -g -lboost_context -lboost_date_time -lboost_thread -lboost_system -lboost_program_optio
ns -lboost_filesystem -o coroutines2 coroutines2.cpp

 

push可以传入参数,pull可以接受参数。

push--pull例子

#include <iostream>
#include <boost/coroutine2/all.hpp>

void foo(boost::coroutines2::coroutine<void>::pull_type & sink1)
{
    std::cout << "a ";
    sink1(); //switch其他线程(主线程)执行
    std::cout << "b ";
    sink1();
    std::cout << "c ";
}

int main()
{
    boost::coroutines2::coroutine<void>::push_type source(foo);
    std::cout << "1 ";
    source();  //调用到source协程执行foo函数
    std::cout << "2 ";
    source();
    std::cout << "3 ";
    return 0;
}

结果:1 a 2 b 3 c

 

pull--push 例子

定义pull的协程时,会先执行pull协程。

#include <iostream>
#include <boost/coroutine2/all.hpp>

void foo(boost::coroutines2::coroutine<void>::push_type & sink)
{
    std::cout << "a ";
    sink(); //switch其他线程(主线程)执行
    std::cout << "b ";
    sink();
    std::cout << "c ";
}
int main()
{
    boost::coroutines2::coroutine<void>::pull_type source(foo);
    std::cout << "1 ";
    source();  //调用到source协程执行foo函数
    std::cout << "2 ";
    source();
    std::cout << "3 ";
    getchar();
    return 0;
}

结果:a 1 b 2 c 3 

 

优化

#include <iostream>
#include <boost/coroutine/all.hpp>

typedef boost::coroutines::asymmetric_coroutine< void >::pull_type pull_coro_t;
typedef boost::coroutines::asymmetric_coroutine< void >::push_type push_coro_t;


void foo(push_coro_t & sink)
{
    std::cout << "1";
    sink();
    std::cout << "2";
    sink();
    std::cout << "3";
    sink();
    std::cout << "4";
}

int main(int argc, char * argv[])
{
    {
        pull_coro_t source(foo);
        while (source)

        {
            std::cout << "-";
            source();
        }
    }

    std::cout << "\nDone" << std::endl;

    return 0;
}

运行输出:

1-2-3-4
Done

 

带返回值的协程:

#include <iostream>
#include <boost/coroutine2/all.hpp>

void foo(boost::coroutines2::coroutine<std::string>::pull_type & sink)
{
    std::cout << "1 get " << sink.get() << " from main() by foo()\n";
    sink();   //switch其他线程(主线程)执行

    //sink.get() 的值已经被其他线程(主线程)修改了
    std::cout << "2 get " << sink.get() << " from main() by foo()\n";
    sink();
}

int main()
{
    std::string str1("HELLO");
    std::string str2("WORLD");
    boost::coroutines2::coroutine<std::string>::push_type source(foo);
    std::cout << "pass " << str1 << " from main() to foo()\n";
    source(str1);   //调用到source协程执行foo函数
    std::cout << "pass " << str2 << " from main() to foo()\n";
    source(str2);
    return 0;
}

结果:

pass HELLO from main() to foo()
1 get HELLO from main() by foo()
pass WORLD from main() to foo()
2 get WORLD from main() by foo()

 

协程的迭代器

协程的迭代器不支持后置++:

#include <iostream>
#include <boost/coroutine2/all.hpp>
#include <boost/coroutine2/detail/push_coroutine.hpp>
#include <boost/coroutine2/detail/pull_coroutine.hpp>

#define N 5

/* 方法一:中规中矩 */
void foo(boost::coroutines2::coroutine<int>::pull_type & sink){
    using coIter = boost::coroutines2::coroutine<int>::pull_type::iterator;
    for (coIter start = begin(sink); start != end(sink);
++start) {
        std::cout << "retrieve "<<*start << "\n";
    }
}
/* 方法二:auto自动推导 */
void foo2(boost::coroutines2::coroutine<int>::pull_type & sink) {
    for (auto val : sink) {
        std::cout << "retrieve " << val << "\n";
    }
}
/* 方法三:守旧 */
void foo3(boost::coroutines2::coroutine<int>::pull_type & sink) {
    for (int i=0; i < N; i++) {
        std::cout << "retrieve " << sink.get() << "\n";
        sink();
    }
}

int main(){
    boost::coroutines2::coroutine<int>::push_type source(foo2);
    for (int i=0; i < N; i++) {
        source(i);
    }

    std::cout << "main end\n";
}

结果:

retrieve 0
retrieve 1
retrieve 2
retrieve 3
retrieve 4
main end

 

https://blog.csdn.net/racaljk/article/details/78507498

 

3.2 魅族libgo

      libgo 是一个使用 C++ 编写的协作式调度的stackful协程库, 同时也是一个强大的并行编程库。

      设计之初是为高并发分布式Linux服务端程序开发提供底层框架支持,可以让链接进程序的同步的第三方库变为异步库,不影响逻辑的前提下提升其性能

目前支持两个平台:

    Linux   (GCC4.8+)

    Windows (Win7、Win8、Win10 x86 and x64 使用VS2013/2015编译)

使用libgo编写并行程序,即可以像golang一样开发迅速且逻辑简洁,又有C++原生的性能优势。

  • 1.提供golang一般功能强大协程,基于corontine编写代码,可以以同步的方式编写简单的代码,同时获得异步的性能

  • 2.支持海量协程, 创建100万个协程只需使用2GB内存

  • 3.允许用户自由控制协程调度点,随时随地变更调度线程数;

  • 4.支持多线程调度协程,极易编写并行代码,高效的并行调度算法,可以有效利用多个CPU核心

  • 5.可以让链接进程序的同步的第三方库变为异步调用,大大提升其性能。再也不用担心某些DB官方不提供异步driver了,比如hiredis、mysqlclient这种客户端驱动可以直接使用,并且可以得到不输于异步driver的性能。

  • 6.动态链接和静态链接全都支持,便于使用C++11的用户静态链接生成可执行文件并部署至低版本的linux系统上。

  • 7.提供协程锁(co_mutex), 定时器, channel等特性, 帮助用户更加容易地编写程序.

  • 8.网络性能强劲,在Linux系统上超越ASIO异步模型;尤其在处理小包和多线程并行方面非常强大

  • 在源码的samples目录下有很多示例代码,内含详细的使用说明,让用户可以很轻易地学会使用libgo。

例子

#include <stdio.h>
#include <libgo/coroutine.h>
  
co_main(int argc, char **argv)
{
    go []{
        printf("1\n");
        co_yield;
        printf("2\n");
    };
      
    go []{
        printf("3\n");
        co_yield;
        printf("4\n");
    };
      
    return 0;
}

 

3.3 腾讯libco

https://github.com/Tencent/libco

      libco是微信后台大规模使用的c/c++协程库,2013年至今稳定运行在微信后台的数万台机器上。

      libco通过仅有的几个函数接口 co_create/co_resume/co_yield 再配合 co_poll,可以支持同步或者异步的写法,如线程库一样轻松。同时库里面提供了socket族函数的hook,使得后台逻辑服务几乎不用修改逻辑代码就可以完成异步化改造。

libco的特性

  • 无需侵入业务逻辑,把多进程、多线程服务改造成协程服务,并发能力得到百倍提升;
  • 支持CGI框架,轻松构建web服务(New);
  • 支持gethostbyname、mysqlclient、ssl等常用第三库(New);
  • 可选的共享栈模式,单机轻松接入千万连接(New);
  • 完善简洁的协程编程接口
  • 类pthread接口设计,通过co_create、co_resume等简单清晰接口即可完成协程的创建与恢复;
  • __thread的协程私有变量、协程间通信的协程信号量co_signal (New);
  • 语言级别的lambda实现,结合协程原地编写并执行后台异步任务 (New);
  • 基于epoll/kqueue实现的小而轻的网络框架,基于时间轮盘实现的高性能定时器;

 

可以参考:

      https://wenku.baidu.com/view/cbbf9726dc36a32d7375a417866fb84ae45cc356.html?re=view

      https://blog.csdn.net/greybtfly/article/category/8277677

      https://blog.csdn.net/GreyBtfly/article/details/83506958

      https://blog.csdn.net/XiyouLinux_Kangyijie/article/details/78494743

     

用到libco的相关项目:

    https://github.com/Tencent/phxsql

    https://github.com/Tencent/phxqueue

 

3.4 开源libaco

https://github.com/hnes/libaco

libaco - A blazing fast and lightweight C asymmetric coroutine library.

The code name of this project is Arkenstone ?

Asymmetric COroutine & Arkenstone is the reason why it's been named aco.

Currently supports Sys V ABI of Intel386 and x86-64.

Here is a brief summary of this project:

  • Along with the implementation of a production-ready C coroutine library, here is a detailed documentation about how to implement a fastest and correct coroutine library and also with a strict mathematical proof;
  • It has no more than 700 LOC but has the full functionality which you may want from a coroutine library;
  • The benchmark part shows that a context switch between coroutines only takes about 10 ns (in the case of standalone stack) on the AWS c5d.large machine;
  • User could choose to create a new coroutine with a standalone stack or with a shared stack (could be shared with others);
  • It is extremely memory efficient: 10,000,000 coroutines simultaneously to run cost only 2.8 GB physical memory (run with tcmalloc, each coroutine has a 120B copy-stack size configuration).

 

4 libco VS libaco

知乎上对比:

     https://www.zhihu.com/question/52193579?sort=created

另外:

      libco有个push:https://github.com/Tencent/libco/issues/90

      感觉里面说的有些道理,可能腾讯内部libco版本已经修改了一些bug。

      所以用一些开源的软件还是要用被维护的,万一有坑就耗时耗力了。

      libaco的性能介绍:https://github.com/hnes/libaco#benchmark


      libaco和腾讯 libco 性能测试对比分支: https://github.com/hnes/libaco/tree/tencent_libco_bug_report_and_coctx_swap_benchmark

 

5 libgo VS libco

 

6 其他开源协程库

     CoroutineTS(C++20)

     tbox

     libcopp

     orchid

     libtask

     libmill

     acl

     state-threads

 

7 总结

      上面介绍的C++协程库可以了解到,协程更多的是对编程方式的改变,对控制流的操控可以用同步的结构写出异步的效果,但是协程是用户态的而不是原生的多线程,所以并不能并行执行提高并发率但是协程能够在各个协程间进行高效的切换,这一点可以做到比传统依赖于异步调度的效率更高,这才体现出协作的本质吧

      如果后面C++项目需要用到协程,我倾向于: libgo > libaco > libco

 

参考:

https://my.oschina.net/yyzybb/blog/1817226

https://tboox.org/cn/2016/10/28/coroutine-context/

https://juejin.im/entry/5c7f22fc518825408b658d1f

https://toutiao.io/posts/d2eeed/preview

https://blog.51cto.com/muhuizz/2328117

http://taozj.net/201609/usage-of-boost-coroutine.html

http://blog.jobbole.com/104789/

  • 6
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值