Boost.Context库简介及协程的构建

 最近从各大公司的开源项目看来,基于协程的高性能服务端开发变得越来越流行了,比如我了解到的微信团队的libco、魅族的libgo、以及libcopp。传统的高性能服务端的开发大多都是基于异步框架和多线程或者多进程的模型来设计的,这种架构虽然经历了长久的考验且经验丰富,但是却有着固有的缺点:
(1). 异步构架将代码逻辑强行分开,不利于人类常理的顺序思维习惯,自然也不是对开发者友好的;
(2). 线程虽然相对于进程共享了大量的数据,创建和切换效率较高,算是作为内核级别轻量级别的调度单元,在X86构架下线程的切换需要大量的CPU指令周期才能完成;同时,当业务增长的时候,如果通过增加工作线程的情况下增加处理能力,反而有可能让系统大部分的资源消耗在线程管理资源和线程调度的开销中去了,获得恰得其反的效果,所以在Nginx中工作进程的数目和CPU执行单元的数目是相同的,通过进程(线程)亲和CPU核的方式,可以最小化进程(线程)切换带来的损失(比如缓存失效等);
(3). 虽然我们某些时候可以通过::sched_yield();主动放弃CPU请求调度,但是被切换进来的线程完全是调度算法决定的,相对于被切换进来的线程是被动的,作为常见的生产——消费者线程模型,两者只能被动苟合而很难做到高效“协作”;
(4). 也是因为上面的原因,线程之间的切换基本都属于用户程序不可控的被动状态,所以很多临界区必须通过加锁的方式进行显式保护才行。
在这种环境下,更加轻量级的协程开发便应运而生,且被各大厂家广为使用了。除了各个研发实力强的大厂开发出服务自己业务的高性能协程库之外,Boost库也发布了Boost.Coroutine2协程库,其中包含了stackless和stackful两种协程的封装,他们的简单使用方法,在我之前的《Boost.Asio中Coroutine协程之使用》已经做了相对比较详细的介绍说明了。这里主要了解介绍一下相对于协程高级接口之下,较为底层中涉及到协程切换过程中资源管理维护之类的基础性东西——Boost.Context库(适用于stackful协程)。
其实协程的实现方式有很多,有能力的大厂可以自己手动进行创建和维护栈空间、保存和切换CPU寄存器执行状态等信息,这些都是跟体系结构密切相关,也会涉及较多的汇编操作,而对于一般的开发者想要快速开发出协程原型,通常采用ucontext或者Boost.Context这现有工具来辅助栈空间和运行状态的管理,ucontext算是历史比较悠久的,通过ucontext_t结构体保存栈信息、CPU执行上下文、信号掩码以及resume所需要的下一个ucontext_t结构的地址,但是人家实测ucontext的性能要比Boost.Context慢的多,Boost.Context是今年来C++各大协程底层支撑库的主流,性能一直在被优化。
Boost.Context所做的工作,就是在传统的线程环境中可以保存当前执行的抽象状态信息(栈空间、栈指针、CPU寄存器和状态寄存器、IP指令指针),然后暂停当前的执行状态,程序的执行流程跳转到其他位置继续执行,这个基础构建可以用于开辟用户态的线程,从而构建出更加高级的协程等操作接口。同时因为这个切换是在用户空间的,所以资源损耗很小,同时保存了栈空间和执行状态的所有信息,所以其中的函数可以自由被嵌套使用。
从我查阅的资料来看来,最近发布的Boost.Context新版本相对老版本更新了很多,抽象出了execution_context的类型,从其内部实现文件可以看出,其实内部的基础结构还是使用的fcontext_t来保存状态,使用make_fcontext、jump_fcontext以及新增的ontop_fcontext来操作之,对过往版本熟悉的大佬们当然可以直接调用这些接口。现在最新的Boost.Context依赖于C++11的一些新特性,而Boost的协程库也针对性的维护了两个版本Boost.Coroutine和Boost.Coroutine2,不知道是不是这个原因所致。
创建execution_context会首先分配一个context stack空间,在其栈顶部保留了维持这个context信息的数据结构,设计中execution_context的环境中不能访问这个数据结构,只能在调用操作符operator()调用的时候其内部状态会自动的更新保存,用户无需关心。正如同boost::thread一样,operator()execution_context也是不支持拷贝的,只支持移动构造和移动赋值操作。
所有的execution_context都需要一个context-function,其函数签名如下:

auto execution_context(execution_context ctx, Args ... args)

  第一个参数ctx是固定的,表明是会在当前context被suspend的时候自动切换resume至的context,通常来说是当前context的创建和调用者,后面的可变参数会自动传递给execution_context::operator()函数作为参数。

Boost.Context的execution_context简单使用的例子

int n = 9;
ctx::execution_context<int> source(
    [n](ctx::execution_context<int> sink, int/*not used*/ ) mutable {
    int a=0, b=1;
    while(n-- >0){
        auto result = sink(a);
        sink = std::move(std::get<0>(result));
        auto next = a + b;
        a = b; b = next;
    }
    return sink;
});
for(int i=0; i<10; ++i) {
    if(source) {
        auto result = source(0);
        source = std::move( std::get<0>(result) );
        std::cout << std::get<1>(result) << " ";  
    }
}
// 输出结果为:0 1 1 2 3 5 8 13 21 0 %

  函数的返回类型跟实例化execution_context的模板参数类型有关:如果suspend和resume两个context之间不需要数据传递而仅仅是控制流的切换,可以使用void实例化execution_context类型创建对象,否则对于resume者来说其接收到的返回值是std::tuple类型,第一个值是suspend的context对象,其余部分是打包好的返回值,如果仅仅返回单个值但是是不同的数据类型,可以考虑使用boost::variant,多个返回值依次封装就可以了

ctx::execution_context<int, std::string> ctx1(
    [](ctx::execution_context<int, std::string> ctx2, int num, std::string) {
    std::string str;
    std::tie(ctx2, num, str) = ctx2(num+9, "桃子是大神");
    return std::move(ctx2);
});
int i = 1;
int ret_j; std::string ret_str;
std::tie(ctx1, ret_j, ret_str) = ctx1(i, "");
std::cout << ret_j << "~" << ret_str << std::endl;

  如果想要在某个被resumed的context上面额外执行自己指定的其他某个函数,可以将调用的第一个参数设置为exec_ontop_arg,然后紧接需要调用的函数,再正常传递context所需要传递的函数,在调用的时候,参数传递给这个指定的函数去执行,同时要求这个函数的返回类型必须是std::tuple封装的可以传递给resume context的参数,然后发生context切换resume使用其参数继续执行。这在新版Boost.Context中引入不久,效果相当于在原来执行context上面添加了一个hook调用。

ctx::execution_context<int> func1(ctx::execution_context<int> ctx, int data) {
 std::cout << "func1: entered first time: " << data << std::endl;
 std::tie(ctx, data) = ctx(data+1);
 std::cout << "func1: entered second time: " << data << std::endl;
 std::tie(ctx, data) = ctx(data+1);
 std::cout << "func1: entered third time(atten): " << data << std::endl;
 return ctx;
}
std::tuple<boost::context::execution_context<int>,int> func2(boost::context::execution_context<int> ctx, int data)
{
    std::cout << "func2: entered: " << data << std::endl;
    return std::make_tuple(std::move(ctx), -3);
}
int main(int argc, char* argv[]){
 int data = 0;
 ctx::execution_context< int > ctx(func1);
 std::tie(ctx, data) = ctx(data+1);
 std::cout << "func1: returned first time: " << data << std::endl;
 std::tie(ctx, data) = ctx(data+1);
 std::cout << "func1: returned second time: " << data << std::endl;
 std::tie(ctx, data) = ctx(ctx::exec_ontop_arg, func2, data+1);
 return 0;
}

上面代码输出的结果显示在下方,data+1==5被传递给func2,然后func2包装了ctx和自己的参数,ctx得到继续执行,使用了func2传递给的参数:

func1: entered first time: 1
func1: returned first time: 2
func1: entered second time: 3
func1: returned second time: 4
func2: entered: 5
func1: entered third time(atten): -3
对象execution_context在创建的时候会分配一个context stack,在context-function返回的时候会被自动析构。
经过追查,发现execute_context是在Boost-1.59中引入的,在其之前的版本还是直接通过联合调用jump_fcontext()、make_fcontext()来操作fcontext_t结构来保存和切换stack和执行状态信息的,虽然现在execution_context封装的更加易用,但是老式的fcontext_t操作结构更加的容易容易理解,感兴趣的想了解更加深入的内容可以查阅老版本的文档。

  之前看Boost.Coroutine的时候,什么call_type、push_type……概念看的眼花缭乱,这里看看协程底层支持的基础框架Boost.Context,有一种豁然开朗的感觉,其实当有人帮你把这些复杂的、依赖于底层架构的东西做完封装好之后,或许期待我有时间的那一天,也能做一个属于自己的协程库,等后面了解一下libgo、libcopp等协程库的原理和思路之后,要不也来造个轮子!

时间: 2024-08-02 00:41:52

Boost.Context库简介及协程的构建的相关文章

谈谈Python协程技术的演进

一.引言 1. 存储器山 存储器山是 Randal Bryant 在<深入理解计算机系统>一书中提出的概念. 基于成本.效率的考量,计算机存储器被设计成多级金字塔结构,塔顶是速度最快.成本最高的 CPU 内部的寄存器(一般几 KB)与高速缓存,塔底是成本最低.速度最慢的广域网云存储(如百度云免费 2T ) 存储器山的指导意义在于揭示了良好设计程序的必要条件是需要有优秀的局部性: 时间局部性:相同时间内,访问同一地址次数越多,则时间局部性表现越佳; 空间局部性:下一次访问的存储器地址与上一次的访

一个使用 asyncio 协程的网络爬虫(二)

协程 还记得我们对你许下的承诺么?我们可以写出这样的异步代码,它既有回调方式的高效,也有多线程代码的简洁.这个结合是同过一种称为协程coroutine的模式来实现的.使用 Python3.4 标准库 asyncio 和一个叫"aiohttp"的包,在协程中获取一个网页是非常直接的( @asyncio.coroutine 修饰符并非魔法.事实上,如果它修饰的是一个生成器函数,并且没有设置 PYTHONASYNCIODEBUG 环境变量的话,这个修饰符基本上没啥用.它只是为了框架的其它部分

ucontext-人人都可以实现的简单协程库

1.干货写在前面 协程是一种用户态的轻量级线程.本篇主要研究协程的C/C++的实现. 首先我们可以看看有哪些语言已经具备协程语义: 比较重量级的有C#.erlang.golang* 轻量级有python.lua.javascript.ruby 还有函数式的scala.scheme等. c/c++不直接支持协程语义,但有不少开源的协程库,如: Protothreads:一个"蝇量级" C 语言协程库 libco:来自腾讯的开源协程库libco介绍,官网 coroutine:云风的一个C语

Lua的协程(coroutine)简介_Lua

协程和多线程下的线程类似:有自己的堆栈,自己的局部变量,有自己的指令指针,但是和其他协程程序共享全局变量等信息.线程和协程的主要不同在于:多处理器的情况下,概念上来说多线程是同时运行多个线程,而协程是通过协作来完成,任何时刻只有一个协程程序在运行.并且这个在运行的协程只有明确被要求挂起时才会被挂起 你可以使用coroutine.create来创建协程: 复制代码 代码如下: co = coroutine.create(function ()      print("hi") end)

协程 [wiki]

协程 维基百科,自由的百科全书 与子例程一样,协程也是一种程序组件.相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛.协程源自Simula和Modula-2语言,但也有其他语言支持.协程更适合于用来实现彼此熟悉的程序组件,如合作式多任务,迭代器,无限列表和管道. 协程最初在1963年被提出.[1] 简单的对比和示例 由于协程不如子例程那样被普遍所知,最好对它们作个比较. 子例程的起始处是惟一的入口点,一旦退出即完成了子例程的执行,子例程的一个实例只会返回一次. 协程可以通过y

PHP协程实现过程详解

多进程/线程 最早的服务器端程序都是通过多进程.多线程来解决并发IO的问题.进程模型出现的最早,从Unix 系统诞生就开始有了进程的概念.最早的服务器端程序一般都是 Accept 一个客户端连接就创建一个进程,然后子进程进入循环同步阻塞地与客户端连接进行交互,收发处理数据. 多线程模式出现要晚一些,线程与进程相比更轻量,而且线程之间共享内存堆栈,所以不同的线程之间交互非常容易实现.比如实现一个聊天室,客户端连接之间可以交互,聊天室中的玩家可以任意的其他人发消息.用多线程模式实现非常简单,线程中可

【译】第一次走进 Android 中的 Kotlin 协程

本文讲的是[译]第一次走进 Android 中的 Kotlin 协程, 原文地址:A first walk into Kotlin coroutines on Android 原文作者:Antonio Leiva 译文出自:掘金翻译计划 译者:Feximin 校对者:wilsonandusa .atuooo 本文提取并改编自最近更新的 Kotlin for Android Developers 一书. 协程是 Kotlin 1.1 引入的最牛逼的功能.他们确实很棒,不但很强大,而且社区仍然在挖掘

Python协程:概念及其用法

真正有知识的人的成长过程,就像麦穗的成长过程:麦穗空的时候,麦子长得很快,麦穗骄傲地高高昂起,但是,麦穗成熟饱满时,它们开始谦虚,垂下麦芒. --蒙田<蒙田随笔全集> 上篇<Python 多线程鸡年不鸡肋>论述了关于python多线程是否是鸡肋的问题,得到了一些网友的认可,当然也有一些不同意见,表示协程比多线程不知强多少,在协程面前多线程算是鸡肋.好吧,对此我也表示赞同,然而上篇我论述的观点不在于多线程与协程的比较,而是在于IO密集型程序中,多线程尚有用武之地. 对于协程,我表示其

一个使用 asyncio 协程的网络爬虫(三)

使用协程 我们将从描述爬虫如何工作开始.现在是时候用 asynio 去实现它了. 我们的爬虫从获取第一个网页开始,解析出链接并把它们加到队列中.此后它开始傲游整个网站,并发地获取网页.但是由于客户端和服务端的负载限制,我们希望有一个最大数目的运行的 worker,不能再多.任何时候一个 worker 完成一个网页的获取,它应该立即从队列中取出下一个链接.我们会遇到没有那么多事干的时候,所以一些 worker 必须能够暂停.一旦又有 worker 获取一个有很多链接的网页,队列会突增,暂停的 wo