最近研究高性能C++协程,网上了解到了魅族libgo、腾讯libco、开源libaco、boost coroutine,这里记录一下。
协程可以很轻量的在子例程中进行切换,它由程序员进行子例程的调度(即切换)而不像线程那样需要内核参与,同时也省去了内核线程切换的开销,因为一个协程切换保留的就是函数调用栈和当前指令的寄存器,而线程切换需要陷入内核态,改变线程对象状态。
go语言就已经把协程作为基础设施提供语言级的支持,cpp这种出了名的给程序员自由的语言肯定不会提供语言级的支持。
各大开源协程库如:libgo, libtask, libmill, boost, libco等,他们都属于stackfull协程,每个协程有完整的私有堆栈,里面的核心就是上下文切换(context),而stackless的协程,比较出名的有protothreads,这个比较另类。
那么现有协程库,是怎么去实现context切换的呢,目前主要有以下几种方式:
各个协程协程库的切换效率的基准测试,可以参考:切换效率基准测试报告
个人认为,C++协程库从实现完善程度上分为以下几个层次
实现协程上下文切换api,或添加一些便于使用的封装; 特点:没有协程调度。
代表作:boost.context, boost.coroutine, ucontext(unix), fiber(windows)
这一层次的协程库,仅仅提供了一个底层api,要想拿来做项目,还有非常非常遥远的距离;不过这些协程api可以为我们实现自己的协程库提供一个良好的基础。
实现了协程调度,无需用户手动处理协程上下文切换;特点:没有HOOK
代表作:libmill
这一层次的协程库,实现了协程调度(类似于操作系统有了进程调度机制);稍好一些的意识到了阻塞网络io与协程的不协调之处,自己实现了一套网络io相关函数;
但是这也意味着涉及网络的第三方库全部不可用了,比如你想用redis?不好意思,hiredis不能用了,要自己轮一个;你想用mysql?不好意思,mysqlclient不能用了,要自己轮一个。放弃整个C/C++生态全部自己轮,这个玩笑开的有点大,所以只能称之为“玩具级”。
以部分正确的方式HOOK了网络io相关的syscall,可以少改甚至不改代码的兼容大多数第三方库;特点:没有完整生态
代表作:libco
这一层次的协程库,但是hook的不够完善,未能完全模拟syscall的行为,只能兼容行为符合预想的同步模型的第三方库,这虽然只能覆盖一部分的第三方库,但是通过严苛的源码审查、付出代价高昂的测试成本,也可以勉强用于实际项目开发了;
但其他机制不够完善:协程间通讯、协程同步、调试等,因此对开发人员的要求很高,深谙底层机制才能写出没有问题的代码;再加上hook不完善带来的隐患,开发过程可谓是步步惊心、如履薄冰。
以100%行为模拟的方式HOOK了网络io相关的syscall,可以完全不改代码兼容大多数第三方库;依照专为协程而生的语言的使用经验,提供了协程开发所必须的完整生态;
代表作:libgo
这一层次的协程库,能够100%模拟被hook的syscall的行为,能够兼容任何网络io行为的同步模型的第三方库;由于协程开发生态的完善,对开发人员的要求变得很低,新手也可以写出高效稳定的代码。但由于C++的灵活性,用户行为是不受限的,所以依然存在几个边边角角的难点需要开发者注意:没有gc(开发者要了解协程的调度时机和生命期),TLS的问题,用户不按套路出牌、把逻辑代码run在协程之外,粗粒度的线程锁等等。
语言级的协程实现
代表作:golang语言
这一层次的协程库,开发者的一切行为都是受限行为,可以实现无死角的完善的协程。
通过准标准库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是相对于Corountine而言的,在Boost v1.59被引入,Boost.Corountine目前已被标记为deprecated,因此不再提及。
Boost.Corountine2使用了Boost.Context,因此要使用Boost.Corountine2,必须先编译Boost.Context。
Boost.Corountine2几个特征:
协程分为对称协程(symmetric)和非对称协程(asymmetric),对称协程需要显式指定将控制权yeild给谁,非对称协程可以隐式的转移控制权给它的调用者,boost coroutine2实现的是非对称协程.
Boost库中的协程支持两种方式:一种是封装了Boost.Coroutine的spawn,是一个stackful类型的协程;一种是asio作者写出的stackless协程。
coroutines2.cpp
#include
#include
int main() {
boost::coroutines2::coroutine::push_type coro(
[&](boost::coroutines2::coroutine::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
#include
#include
void foo(boost::coroutines2::coroutine
{
std::cout << "a ";
sink1(); //switch其他线程(主线程)执行
std::cout << "b ";
sink1();
std::cout << "c ";
}
int main()
{
boost::coroutines2::coroutine
std::cout << "1 ";
source(); //调用到source协程执行foo函数
std::cout << "2 ";
source();
std::cout << "3 ";
return 0;
}
结果:1 a 2 b 3 c
定义pull的协程时,会先执行pull协程。
#include
#include
void foo(boost::coroutines2::coroutine
{
std::cout << "a ";
sink(); //switch其他线程(主线程)执行
std::cout << "b ";
sink();
std::cout << "c ";
}
int main()
{
boost::coroutines2::coroutine
std::cout << "1 ";
source(); //调用到source协程执行foo函数
std::cout << "2 ";
source();
std::cout << "3 ";
getchar();
return 0;
}
结果:a 1 b 2 c 3
#include
#include
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
#include
void foo(boost::coroutines2::coroutine
{
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::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
#include
#include
#include
#define N 5
/* 方法一:中规中矩 */
void foo(boost::coroutines2::coroutine
using coIter = boost::coroutines2::coroutine
for (coIter start = begin(sink); start != end(sink);
std::cout << "retrieve "<<*start << "\n";
}
}
/* 方法二:auto自动推导 */
void foo2(boost::coroutines2::coroutine
for (auto val : sink) {
std::cout << "retrieve " << val << "\n";
}
}
/* 方法三:守旧 */
void foo3(boost::coroutines2::coroutine
for (int i=0; i < N; i++) {
std::cout << "retrieve " << sink.get() << "\n";
sink();
}
}
int main(){
boost::coroutines2::coroutine
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
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
#include
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;
}
https://github.com/Tencent/libco
libco是微信后台大规模使用的c/c++协程库,2013年至今稳定运行在微信后台的数万台机器上。
libco通过仅有的几个函数接口 co_create/co_resume/co_yield 再配合 co_poll,可以支持同步或者异步的写法,如线程库一样轻松。同时库里面提供了socket族函数的hook,使得后台逻辑服务几乎不用修改逻辑代码就可以完成异步化改造。
可以参考:
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
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:
知乎上对比:
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
CoroutineTS(C++20)
tbox
libcopp
orchid
libtask
libmill
acl
state-threads
上面介绍的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/