现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

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

2019-04-25 15:35 工业·编程 ⁄ 共 9715字 ⁄ 字号 暂无评论

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

1 什么是协程

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

相关阅读

    ----谈谈对协程的理解

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

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

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

使用ucontext系列接口,例如:libtask

使用setjmp/longjmp接口,例如:libmill

使用boost.context,纯汇编实现,内部实现机制跟ucontext完全不同,效率非常高,后面会细讲,tbox最后也是基于此实现

使用windows的GetThreadContext/SetThreadContext接口

使用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在协程之外,粗粒度的线程锁等等。

2.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

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的特性

1)无需侵入业务逻辑,把多进程、多线程服务改造成协程服务,并发能力得到百倍提升;

2)支持CGI框架,轻松构建web服务(New);

3)支持gethostbyname、mysqlclient、ssl等常用第三库(New);

4)可选的共享栈模式,单机轻松接入千万连接(New);

5)完善简洁的协程编程接口

6)类pthread接口设计,通过co_create、co_resume等简单清晰接口即可完成协程的创建与恢复;

7)__thread的协程私有变量、协程间通信的协程信号量co_signal (New);

8)语言级别的lambda实现,结合协程原地编写并执行后台异步任务 (New);

9)基于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 libgo VS libco

2019042911312297

5 其他开源协程库

     CoroutineTS(C++20)

     tbox

     libcopp

     orchid

     libtask

     libmill

     acl

     state-threads

6 总结

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

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

作者:zzhongcy

给我留言

留言无头像?