基于汇编的 C/C++ 协程 - 背景知识
近几年来,协程在 C/C++ 服务器中的解决方案开始涌现。本文主要阐述以汇编实现上下文切换的协程方案,并且说明其在异步开发模式中的应用。
本文地址:https://segmentfault.com/a/1190000013070736
首先,我们来看一下 C/C++ 服务器开发的历史。
参考资料
- 协程 - 维基百科,自由的百科全书
- 异步IO - 维基百科,自由的百科全书
- 基于 epoll 设计类似 libevent 的异步 I/O 库 - 接口
- 系统调用真正的效率瓶颈在哪里?
- python协程是什么?——这个讨论页其实不单论 Python,其实大部分是从语言无关的角度回答了协程是什么
传统的 C/C++ 服务器设计框架
同步 I/O 框架
长期以来,使用 C/C++ 编写服务器程序的时候,往往使用的是多进程模式:一个父进程负责 accept
传入连接,然后 fork
一个子进程处理;或者是一个父进程创建了一个 socket
之后,fork
出多个子进程同时执行 accept
和处理。
为什么说这是同步呢?因为这个设计思路,完全就是教科书般的、对于 socket 处理的思路。这个思路我在我关于 libev 的介绍文后的评论中也提及:
- 科班出身的软件专业往往会简单学到 socket, bind, connect, accept, read, write 等等一路下来的 API。正好,如果真的是按顺序这样调用下来,然后写了一个 server / client 的话,可以算是一种同步 I/O 的编程思路。
程序执行的每一步系统调用,都会阻塞住(直接的结果就是导致进程切换),等待远端机器的响应,并且直到数据到达之后,才会执行下一步。这就是典型的同步(阻塞) I/O。
上面的每步流程如果简单写下来的话,支撑不起高并发,因为阻塞的存在。为了解决这个问题,加入 fork
,就可以实现对多个客户端的服务了。
同步开发模式
同步 I/O 框架,使用的是同步开发模式。人的思维,是同步化的,先做什么后做什么,都是一条线式的流程。这样一条线从头到尾的开发模式,就是同步开发模式,非常符合人的思路习惯,便于设计、理解。
同步 I/O 的优势
- 简单、一目了然——同步 I/O 框架中,使用同步开发模式,因此设计出来的程序代码简洁、明确。
- bug 少——其实这上面已经讲了,程序简洁、明确、易于理解,也就容易 debug 了。
- 柔性高——一个服务以一个进程的模式存在,如果程序有 bug,崩溃了,也不会影响到其他的服务。此外,如果子进程是处理完连接就直接
exit
退出的话,那么几乎不用考虑内存泄露的问题——进程创建的所有资源都会被操作系统回收。
同步 I/O 的劣势
- 效率低——
fork
设计进程间切换,这是一个需要陷入内核的操作,耗时长,对于高并发场景,对服务器资源的利用效率很低。 - 进程间同步复杂——进程什么时候占用 CPU、什么时候被切换掉,这是无法预料的,因此进程间需要做好同步。多进程同步的 debug 是非常非常复杂的。
- 进程间通信复杂——这个没什么好说的,进程间通信,够写一本书了。这一点,在各任务之间还需要通信的场景中,反而加大了开发复杂度。
吐槽一下,本人进入工作后就见到的第一个服务器就是基于 libevent 设计的,并且整个团队都一直这么设计,以至于我曾经以为同步 I/O 根本没人用……
异步 I/O 框架
首先讲从技术层面的 “异步 I/O 框架” 是怎么回事。维基百科上对 “异步 IO” 的定义是:
- 发起IO请求的线程不等IO操作完成,就继续执行随后的代码,IO结果用其他方式通知发起IO请求的程序。
也就是说,某个进程 or 线程,需要告诉操作系统:我需要在某个文件描述符或句柄上 read
或 write
,但是进程 or 线程并不等待 read
或 write
ready,而是等到真正有数据可读或可写入数据的时候,再执行相应的操作。
其实 read
/ write
一早就在理论上对这样的操作提供了支持,那就是 O_NONBLOCK
标志。当对 socket 设置了该标志后,如果执行 read / write,资源暂时不可用的话,会返回响应的错误。此时,程序就可以跳过这个句柄,去查看下一个资源了。
但这个方案显然是不现实的,因为当客户端数量很大的时候,对所有的资源都需要进行轮询操作,这是对 CPU 时间的极大浪费,也极大地拉低了服务的响应速度。因此,操作系统需要提供定义的后半段:“通知”。
实现 “通知” 的办法,其实就是一个系统调用:select
。其实 select 的效率很低,一般操作系统会提供替代。对于 Linux 而言,就是 epoll
。关于异步 I/O 原理和编程,我的文章有很多了,可以点击这里查看。
从技术层面上,异步 I/O 框架有以下的优势:
- 效率高——判断那个资源上的事件 ready,至少是 O(NlogN) 的复杂度,效率极高
- 单线程多任务——单一一个线程就可以处理多个传入请求,达到伪并行的效果,没有进程/线程切换,大大提高了处理速度,优化了 CPU 使用率
- 同一线程中没有同步问题——同一线程的多任务如果需要相互通信,那么完全没有竞争和同步的问题
然而,单线程多任务其实也是很大的一个劣势——多个任务都在一个线程 / 进程中处理,如果程序有 bug,那么整个进程都会崩溃,这对服务器的开发质量要求很高。
异步开发模式
异步 I/O 框架,大部分使用的就是异步开发模式。我们先不用这个词汇吧,换成大家比较熟悉的词。下面两个词,其实都可以解释什么叫异步开发模式:
- 基于事件驱动的开发模式
- 状态机编程
异步开发模式它是基于事件驱动的,当什么事件到来,就调用哪个回调进行处理——或者是回调判断发生了什么事件,再调用不同的函数处理。这与我们传统的思维不同,因此很大程度上,我们需要画状态机,才能很好地解释我们的软件逻辑。
异步开发模式的缺点
其实,异步开发的世界中,满是各种回调以及回调的注册。如果我们不是相应的业务代码的开发者,那么走读代码时,看到一段函数执行完后,我们根本不知道这段函数的调用方是谁,从而也就无法跟踪判断下一段代码是什么。
这就给调试带来了极大的困难。其实即便是程序的开发者,如果文档不足的话,当时间长了之后,恐怕也会忘记自己当时的业务逻辑了吧……因此,异步开发模式对开发者的水平和团队编程风格的要求很高。
异步开发模式的优点
但是异步开发模式也有很大的优点,那就是状态机编程。这其实很好理解,对于那种逻辑并不是一整条简单的直线,而是有着非常多的分叉——有很多外部触发条件、并且会导致很多不同状态切换的程序而言,异步开发模式简直是福音。
示例
比如电梯,一个正运行中的电梯,其执行逻辑很容易被某一楼层用户按下按钮这一动作中断。电梯需要对用户的操作进行及时的响应,以决定自己接下来应该采取什么操作。
一个电梯,至少有以下几个阶段,中断可能发生在电梯运行中的任何一个阶段:
- 电梯静止,门已经关闭
- 电梯静止,门已经打开
- 电梯静止,门正在打开
- 电梯静止,门正在关闭
- 电梯匀速运行
- 电梯加速运行
- 电梯减速运行
- 电梯故障异常
- 电梯检修
此外,中断的类型还可能是多种多样:
- 管理员直接下达指令操作
- 同楼层用户按下
- 不同楼层用户按下
如果使用同步开发模式,这样的逻辑简直是灾难!
协程
前文我刻意将同步开发模式和同步 I/O、异步开发模式和异步 I/O 分开来说明。确实,开发模式和技术手段是两码事。在逻辑比较线性的(相比起上面 “电梯” 的例子)服务(特别是海量服务)而言,我们最理想的开发方案就是:
- 使用同步开发模式——最适合人脑的思维方式,同时也便于进行程序的调试和 debug
- 使用异步 I/O 技术——效率最高的底层实现
曾经我以为这两者的结合在 C/C++ 上是无法实现的,直到我换了东家之后才知道,原来可以这么玩——
协程简介
协程,作为一种服务器组件,在多种高级语言中存在。相比起线程和进程而言,它的切换非常速度快(不用陷入内核态,没有系统调用),很适合在海量服务中使用。
但是在以 C/C++ 为主的中级语言服务器开发中,一直没有大规模引入。原因是,C/C++ 实在是太接近底层了,汇编后的目标文件,直接就是汇编语言代码;而汇编语言的下面,则直接就是虚拟内存了,能够对其施加影响的,只有操作着更加底层(硬件寄存器)的操作系统。
但是其他高级语言不同,比如 Java
。Java 在原理上是解释型语言,但是从开发者的角度,其实和编译型语言无异,只是它把代码编译成了由 JVM
可以识别的程序罢了。这样,在真正执行的程序(二进制代码)和程序代码之间,JVM 可以提供一个中间层——以往由操作系统执行的任务调度和上下文切换,JVM 可以接管过来,在用户态中完成。这就是协程的实现。
协程原理
协程的实现,涉及两个内容:
- 协程调度
- 上下文切换
C/C++ 协程的调度
协程调度的原理,往大了说,其实和线程 / 进程的调度原理无异。这里分抢占和非抢占两种了。对于 C/C++ 而言。
要实现抢占式很难,而且也没太大必要,因为花了很大力气实现抢占式的协程调度,反而失去了前文提到的 “同一线程中没有同步问题” 这一优势了。
所以,针对 C/C++ 协程,最好的方式就是使用非抢占式调度,需要任务通过某些调用主动让出 CPU 使用权。再进一步具体化到服务器编程中,由于每一个合法的传入连接的优先级是相同的,因此只需要使用基于 epoll
的实现来进行简单调度就行了。
基于汇编实现的 C/C++ 协程的上下文切换
上下文切换,是 C/C++ 协程的一大难题,这也是导致了 C/C++ 长期没有可用的、统一的协程库的原因。这一部分行文比较长,我还是放在下一篇文章里面讲吧。
https://segmentfault.com/a/1190000013070736
基于汇编的 C/C++ 协程 - 背景知识的更多相关文章
- 基于汇编的 C/C++ 协程 - 切换上下文
在前一篇文章<基于汇编的 C/C++ 协程 - 背景知识>中提到一个用于 C/C++ 的协程所需要实现的两大功能: 协程调度 上下文切换 其中调度,其实在技术实现上与其他的线程.进程调度没 ...
- python基础===基于requests模块上的协程【trip】
今天看博客get了一个有趣的模块,叫做 trip #(pip install trip) 兼容2.7版本 基于两大依赖包:TRIP: Tornado & Requests In Pa ...
- PHP的生成器、yield和协程
虽然之前就接触了PHP的yield关键字和与之对应的生成器,但是一直没有场景去使用它,就一直没有对它上心的研究.不过公司的框架是基于php的协程实现,觉得有必要深入的瞅瞅了. 由于之前对于生成器接触不 ...
- 协程库st(state threads library)原理解析
协程库state threads library(以下简称st)是一个基于setjmp/longjmp实现的C语言版用户线程库或协程库(user level thread). 这里有一个基本的协程例子 ...
- 11.python3标准库--使用进程、线程和协程提供并发性
''' python提供了一些复杂的工具用于管理使用进程和线程的并发操作. 通过应用这些计数,使用这些模块并发地运行作业的各个部分,即便是一些相当简单的程序也可以更快的运行 subprocess提供了 ...
- python协程总结
概述 python多线程中因为有GIL(Global Interpreter Lock 全局解释器锁 )的存在,所以对CPU密集型程序显得很鸡肋:但对IO密集型的程序,GIL会在调用IO操作前释放,所 ...
- Python核心技术与实战——十六|Python协程
我们在上一章将生成器的时候最后写了,在Python2中生成器还扮演了一个重要的角色——实现Python的协程.那什么是协程呢? 协程 协程是实现并发编程的一种方式.提到并发,肯很多人都会想到多线程/多 ...
- 【python之路36】进程、线程、协程相关
线程详细用法请参考:http://www.cnblogs.com/sunshuhai/articles/6618894.html 一.初始多线程 通过下面两个例子的运行效率,可以得知多线程的速度比单线 ...
- Python3的原生协程(Async/Await)和Tornado异步非阻塞
原文转载自「刘悦的技术博客」https://v3u.cn/a_id_113 我们知道在程序在执行 IO 密集型任务的时候,程序会因为等待 IO 而阻塞,而协程作为一种用户态的轻量级线程,可以帮我们解决 ...
随机推荐
- Android - 序列化与反序列化
http://www.cnblogs.com/yezhennan/p/5527506.html http://blog.csdn.net/wangchunlei123/article/details/ ...
- Android四大组件framework层
activity https://www.kancloud.cn/alex_wsc/android-deep2/413484 当前Activity Activity向AMS发送StartActivit ...
- Application作用域实现:当用户重复登录时,挤掉原来的用户
Application作用域实现:当用户重复登录时,挤掉原来的用户 一.实现思想 1.application(ServletContext)是保存在服务器端的作用域,我们在application中保存 ...
- Hadoop在启动时的坑——start-all.sh报错
1.若你用的Linux系统是CentOS的话,这是一个坑: 它会提示你JAVA_HOME找不到,现在去修改文件: .修改hadoop配置文件,手动指定JAVA_HOME环境变量 [${hadoop_h ...
- art-template辅助函数和子模板
art-template 前端使用 用途:主要用来处理数据和优化性能,与其他的一些模块化处理数据的插件相比,art-template处理性能好 不废话,上代码 1.art-template基本语法使用 ...
- Windows10设置
按下Win+R键,输入gpedit.msc,打开组策略窗口
- 01-01基于SHELL的数据分析
#!/usr/bin/env bash for year in /root/Downloads/data/all/* do echo -ne `basename $year .gz`"\t& ...
- css实现3D立方体旋转特效
先来看运行后出来的效果 它是在不停运行的一个立方体 先来看html部分的代码 <div class="rect-wrap"> <!--舞台元素,设置perspec ...
- Python 排错UnicodeEncodeError 'ascii' codec can't encode character 错误解决方法
Python UnicodeEncodeError 'ascii' codec can't encode character 错误解决方法 by:授客 QQ:1033553122 错误描述: py ...
- VMware Linux虚拟机与WIN7操作系统共享无线网络上网配置
Linux虚拟机与WIN7操作系统共享无线网络上网配置 by:授客 QQ:1033553122 测试环境: CentOS-7-x86_64-DVD-1503-01.iso Vmware 9 实践操作: ...