最近研究高性能C++协程,网上了解到了魅族libgo、腾讯libco、开源libaco、boost 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
5 其他开源协程库
CoroutineTS(C++20)
tbox
libcopp
orchid
libtask
libmill
acl
state-threads
6 总结
上面介绍的C++协程库可以了解到,协程更多的是对编程方式的改变,对控制流的操控可以用同步的结构写出异步的效果,但是协程是用户态的而不是原生的多线程,所以并不能并行执行提高并发率。但是协程能够在各个协程间进行高效的切换,这一点可以做到比传统依赖于异步调度的效率更高,这才体现出协作的本质吧!
如果后面C++项目需要用到协程,我倾向于: libgo > libaco > libco
作者:zzhongcy