协程 及 libco 介绍

libco 是腾讯开源的一个协程库,主要应用于微信后台RPC框架,下面我们从为什么使用协程、如何实现协程、libco使用等方面了解协程和libco。

why协程

为什么使用协程,我们先从server框架的实现说起,对于client-server的架构,server最简单的实现:

while(1) {accept();recv();do();send();}

串行地接收连接、读取请求、处理、应答,该实现弊端显而易见,server同一时间只能为一个客户端服务。

为充分利用好多核cpu进行任务处理,我们有了多进程/多线程的server框架,这也是server最常用的实现方式:

accept进程 - n个epoll进程 - n个worker进程

  1. accpet进程处理到来的连接,并将fd交给各个epoll进程
  2. epoll进程对各fd设置监控事件,当事件触发时通过共享内存等方式,将请求传给各个worker进程
  3. worker进程负责具体的业务逻辑处理并回包应答

以上框架以事件监听、进程池的方式,解决了多任务处理问题,但我们还可以对其作进一步的优化。

进程/线程是Linux内核最小的调度单位,一个进程在进行io操作时 (常见于分布式系统中RPC远程调用),其所在的cpu也处于iowait状态。直到后端svr返回,或者该进程的时间片用完、进程被切换到就绪态。是否可以把原本用于iowait的cpu时间片利用起来,发生io操作时让cpu处理新的请求,以提高单核cpu的使用率?

协程在用户态下完成切换,由程序员完成调度,结合对socket类/io操作类函数挂钩子、添加事件监听,为以上问题提供了解决方法。

用户态下上下文切换

Linux提供了接口用于用户态下保存进程上下文信息,这也是实现协程的基础:

  • getcontext(ucontext_t *ucp): 获取当前进程/线程上下文信息,存储到ucp中
  • makecontext(ucontext_t *ucp, void (*func)(), int argc, ...): 将func关联到上下文ucp
  • setcontext(const ucontext_t *ucp): 将上下文设置为ucp
  • swapcontext(ucontext_t *oucp, ucontext_t *ucp): 进行上下文切换,将当前上下文保存到oucp中,切换到ucp

以上函数与保存上下文的 ucontext_t 结构都在 ucontext.h 中定义,ucontext_t 结构中,我们主要关心两个字段:

  • struct ucontext *uc_link: 协程后继上下文
  • stack_t uc_stack: 保存协程数据的栈空间

stack_t 结构用于保存协程数据,该空间需要事先分配,我们主要关注该结构中的以下两个字段:

  • void __user *ss_sp: 栈头指针
  • size_t ss_size: 栈大小

获取进程上下文并切换的方法,总结有以下几步:

  1. 调用 getcontext(),获取当前上下文
  2. 预分配栈空间,设置 xxx.uc_stack.ss_sp 和 xxx.uc_stack.ss_size 的值
  3. 设置后继上下文环境,即设置 xxx.uc_link 的值
  4. 调用 makecontext(),变更上下文环境
  5. 调用 swapcontext(),完成跳转

Socket族函数/io异步处理

当进程使用socket族函数 (connect/send/recv等)、io函数 (read/write等),我们使用协程切换任务前,需对相应的fd设置监听事件,以便io完成后原有逻辑继续执行。

对io函数,我们可以事先设置钩子,在真正调用接口前,对相应fd设置事件监听。同样,Linux为我们设置钩子提供了接口,以read()函数为例:

  1. 编写名字为 read() 的函数,该函数先对fd调用epoll函数设置事件监听
  2. read() 中使用dlsym(),调用真正的 read()
  3. 将编写好的文件打包,编译成库文件:gcc -shared -Idl -fPIC prog2.c -o libprog2.so
  4. 执行程序时引用以上库文件:LD_PRELOAD=/home/qspace/lib/libprog2.so ./prog

当在prog程序中调用 read() 时,使用的就是我们实现的 read() 函数。

对于glibc函数设置钩子的方法,可参考:Let‘s Hook a Librarg Function

libco

有了以上准备工作,我们可以构建这样的server框架:

accept进程 - epoll进程(n个epoll协程) - n个worker进程(每个worker进程n个worker协程)

该框架下,接收请求、业务逻辑处理、应答都可以看做单独的任务,相应的epoll、worker协程事先分配,服务流程如下:

  1. mainloop主循环,负责 i/监听请求事件,有请求则拉起一个worker协程处理;ii/如果timeout时间内没有请求,则处理就绪协程(即io操作已返回) 
  2. worker协程,如果遇到io操作则挂起,对fd加监听事件,让出cpu

libco 提供了以下接口:

  • co_create: 创建协程,可在程序启动时创建各任务协程
  • co_yield: 协程主动让出cpu,调io操作函数后调用
  • co_resume: io操作完成后(触发相应监听事件)调用,使协程继续往下执行

socket族函数(socket/connect/sendto/recv/recvfrom等)、io函数(read/write) 在libco的co_hook_sys_call.cpp中已经重写,以read为例:

ssize_t read( int fd, void *buf, size_t nbyte )

{
    struct pollfd pf = { 0 };
    pf.fd = fd;
    pf.events = ( POLLIN | POLLERR | POLLHUP ); 

    int pollret = poll( &pf,1,timeout );  /*对相应fd设置监听事件*/
    ssize_t readret = g_sys_read_func( fd,(char*)buf ,nbyte );   /*真正调用read()*/
    return readret;
}

小结

由最简单的单任务处理,到多进程/多线程(并行),再到协程(异步),server在不断地往极致方向优化,以更好地利用硬件性能的提升(多核cpu的出现、单核cpu性能不断提升)。

对程序员而言,可时常检视自己的程序,是否做好并行与异步,在硬件性能提升时,程序服务能力可不可以有相应比例的提升。

时间: 2024-10-23 14:09:22

协程 及 libco 介绍的相关文章

写个百万级别full-stack小型协程库——原理介绍

其实说什么百万千万级别都是虚的,下面给出实现原理和测试结果,原理很简单,我就不上图了: 原理:为了简单明了,只支持单线程,每个协程共享一个4K的空间(你可以用堆,用匿名内存映射或者直接开个数组也都是可以的,总之得保证4K页对齐的空间),每个协程自己有私有栈空间指针privatestackptr,每个时刻只有一个协程在运行,此时栈空间在这个4K共享空间中(当然除了main以外),当切换协程时,动态分配一个堆内存,大小为此时协程栈实际大小(一般都很小,小的只有几十个Bytes, 大的有几百个Byte

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

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

简单介绍什么是协程及其在ES6中的实现方式

协程,英文名coroutine,是一种执行过程可以被暂停和恢复的方法.各个协程之间相互协作完成一个任务. 让我们来看一个关于发挥协程作用的例子.假定我们有一个生产者和消费者的关系,生产者创建物品并将物品添加到一个队列,消费者从队列中取出物品并使用该物品.为了提高效率,生产者会一直创建并添加物品,直到队列满为止,队列满时通知运行环境调用消费者:消费者会一直取出并使用物品,直到队列空为止,队列空时通知运行环境调用生产者.下面是使用协程实现这个关系的伪代码: var q := new queue co

python:协程的介绍

cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制): ?一种情况是该任务发生了阻塞: 另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它. 1.什么是协程? 协程本质上就是一个线程,以前线程任务的切换是由操作系统控制的,遇到I/O自动切换,现在我们用协程的目的就是较少操作系统切换的开销(开关线程,创建寄存器.堆栈等,在他们之间进行切换等),在我们自己的程序里面来控制任务的切换. 2.通过yield实现协程 上述第二种情况并不能提升效率,只是为了让

Kotlin 协程真的比 Java 线程更高效吗?

本文首发于 vivo互联网技术 微信公众号? 链接:https://mp.weixin.qq.com/s/-OcCDI4L5GR8vVXSYhXJ7w 作者:吴越 网上几乎全部介绍Kotlin的文章都会说Kotlin的协程是多么的高效,比线程性能好很多,然而事情的真相真是如此么? 协程的概念本身并不新鲜,使用C++加上内嵌汇编,一个基本的协程模型50行代码之内就可以完全搞出来.早在2013年国内就有团队开源了号称支持千万并发的C++协程库 libco. 最近几年协程的概念越来越深入人心,主要还是

Python实现基于协程的异步爬虫

一.课程介绍 1. 课程来源 本课程核心部分来自<500 lines or less>项目,作者是来自 MongoDB 的工程师 A. Jesse Jiryu Davis 与 Python 之父 Guido van Rossum.项目代码使用 MIT 协议,项目文档使用 http://creativecommons.org/licenses/by/3.0/legalcode 协议. 课程内容在原文档基础上做了稍许修改,增加了部分原理介绍,步骤的拆解分析及源代码注释. 2. 内容简介 传统计算机

python线程、进程和协程

链接:http://www.jb51.net/article/88825.htm 引言 解释器环境:python3.5.1 我们都知道python网络编程的两大必学模块socket和socketserver,其中的socketserver是一个支持IO多路复用和多线程.多进程的模块.一般我们在socketserver服务端代码中都会写这么一句: server = socketserver.ThreadingTCPServer(settings.IP_PORT, MyServer) Threadi

python之线程、进程和协程

引言 解释器环境:python3.5.1我们都知道python网络编程的两大必学模块socket和socketserver,其中的socketserver是一个支持IO多路复用和多线程.多进程的模块.一般我们在socketserver服务端代码中都会写这么一句:server = socketserver.ThreadingTCPServer(settings.IP_PORT, MyServer)ThreadingTCPServer这个类是一个支持多线程和TCP协议的socketserver,它的

魅族C++协程框架(Kiev)技术内幕

Kiev框架简介 kiev是魅族科技推送平台目前使用的Linux-C++后台开发框架.从2012年立项起,先后由多位魅族资深架构师.资深C++工程师倾力打造,到本文写就的时间为止,已经在推送平台这个千万用户级的大型分布式系统上经历了近5年的考验.如今Kiev在魅族推送平台中,每天为上百个服务完成数百亿次RPC调用. kiev作为一套完整的开发框架,是专为大型分布式系统后台打造的C++开发框架,由以下几个组件组成: RPC框架(TCP/UDP) FastCGI框架 redis客户端(基于hired