书名:《0 bug:C/C++商用工程之道》
作者:肖舸
读完:2020-03-12 · 微信读书
这本书讲 C/C++ 商用工程怎样少出错。作者先讲商用程序员该有的思维和系统边界,再讲日志、内存、锁和池这些基础,最后落到无错化程序设计的习惯上。
下面是我的划线,按主题整理。
一、商用程序员的思维
拥有商用程序开发思维的程序员,会更加关心工程项目的中心思想,核心的客户需求,随时随地判断自己所做的工作,对整个项目的实现是否起到正面的作用,一切的优化方向,是否贴合需求所需要的业务数据类型以及使用方向。
笔者有个经验,一个商用系统设计完成,随便拿一个性能参量,比如吞吐率,以时间轴画一条测试曲线,这条曲线,一般应该很平滑,不要有尖锐的波峰和波谷,这是一种比较好的状态。
什么是边界?一个系统最多有多少台设备?多少个用户?平时的工作流程,并发访问有多少?每台服务器、每台客户端,最多可以有多大内存,多快的CPU,硬盘多大?这些是大边界,必须第一时间和客户确定清楚。
原因很简单,边界决定数据结构,边界决定算法。
商用程序员,永远不会相信一条链路、一个模块会永远完好,也不会相信一次调用一定会成功,他们会设置很多校验机制,会仔细检查每次调用返回的结果,随时判定服务的成功与否,并处理每一个错误的情况,书写具有较高自恢复性的程序逻辑,保证系统在任何错误下都能自动恢复,持续服务。
商用软件系统工程,一般强调的是智能化的异常处理,异常之后的自恢复特性,任何一次异常造成的某一次客户服务失败,都不算bug,只要程序能自动恢复,无资源泄漏,无崩溃,无累积性逻辑错误,系统就是成功的。
二、基本开发思路
服务器必须自带Log系统,Log系统须充分考虑磁盘写满、文件超限等情况,自身设计非常安全。Log系统执行静默原则,即有错误则执行记录,没有错误不做任何事。如无特殊要求,原则上,服务器的Log系统保留最少72小时的记录,甚至更长,以方便故障分析。
由于前述原因,且由于线程启动时,需要分配一定的栈空间(Linux下为1MB),嵌入式系统原则上并发的线程数一般不允许超过50条(一般建议小于30条),且每个C函数内部,不允许使用超过1MB的静态数组。
所有的模块,内部不允许使用大型静态数组,尽量动态内存静态管理,以保证有限的栈资源调用,以及保证边界可调,方便各个平台条件编译。
根据商业化系统工程常见的需求分析,跨平台传输库至少包含内存池、线程池、log日志系统、基本队列、基本传输等关键模块。
设计原则:数据传输内核提供通路,但不保证传输,传输内核不负责校验数据的正确性,传输品质保证由上层应用模块负责。
如果读者以后面临类似提升服务器性能的需求,可以首先考虑拆分业务,将每笔业务都拆分成为单一服务,然后通过异步运算提升整体服务器集群的性能和容量。
三、内存、进程与锁
我们用到的malloc和new都是从堆空间申请的内存,new比malloc多了对象的支持,可以自动调用构造函数。另外,这里也可以看出,new创建的对象,其成员变量位于堆里面。
正解是启动函数malloc一块内存区域,将需要传递的参数填入,将指针传入线程,线程函数收到后使用,最后线程退出时,free释放(C++的new和delete [ ]也可以)。
操作系统启动进程,一般都会做配置运行表,分配一块私有内存空间,读入二进制程序,地址修订,分配时间片,跳转到代码开始处执行等工作。多任务操作系统允许同时启动多个进程。
使用锁是不同线程访问相同资源的必要条件,否则程序的运行结果将不可知。
写多读锁,简单说,就是对资源的访问分为两种状态,一种是读操作,另一种是写操作。由应用程序提示锁应该做哪种操作。当为读模式时,所有的写动作被悬挂,而读请求被允许通过,而写动作时,所有操作被悬挂。并且,读写切换时,有足够的状态等待,直到真正安全时,才会切换动作
锁其实是为了解决并发访问时乱序写动作造成的数据不一致问题
Windows对于同一线程的Double Lock不报错的缺陷,在Linux下则会导致程序挂死,因此在锁调用时必须严密关注。
Windows下一般使用VC开发,Linux下则使用gcc开发,笔者一般的习惯是同一段代码,在VC下编译运行通过后,马上利用Linux虚拟机验证其Linux正确性。
四、“池”
书里对“池”的定义:
……其实就是对于一个与业务相关的结构、算法或动作在高度抽象化之后的资源集合。
池在商用工程实施过程中通常在后期优化阶段时才被引入,即先用普通方式解决问题,待性能测试发现瓶颈后考虑使用池技术来缓解。这也是商用工程“先解决有无问题,再解决好坏问题”的体现。
C/C++语言商用实战中,常见的池有内存池、线程池、任务池、主动连接管理池和被动连接管理池等。
五、无错化程序设计
简单说来,内存块泄漏、socket泄漏、指针泄漏,几乎是所有C和C++程序员都遇到的必然bug。
严格按照系统分析步骤走,先画拓扑图再画时序图,数据流程描述等必须完善,模块设计至少做三层,关键算法一定有流程图保证,核心数据确保在设计阶段确认完毕,根据项目特点,与客户、主管、同事充分讨论沟通,确保无逻辑硬伤。
养成良好的开发习惯,包括抽象思维,每个逻辑只写一遍,尽量通用,良好的命名规范,良好的编程习惯,固定一种有效的程序设计风格,不要变来变去。每个函数、模块做好防御性设计,对输入参数严格校验,不符合规范即拒绝服务。
做事情坚持“先解决有无问题,再解决好坏问题”的原则,永远是先实现后优化。
建立程序自纠错机制,所有“艰难代码”自带debug功能,可以简单实现bug的自动报警和输出。重点是各种资源(内存、socket、锁等资源)的监视和控制。建立完善的debug和log日志系统,系统状态随时输出调试。
六、对象、函数式与一段代码
对象就是一些数据,以及针对这些数据所有方法的集合
主要是函数式开发中很少用到变量,所有的计算中间值基本上都是固化的常量,因此,后续开发基本上采用只读方式来访问中间值。我们知道,只读线程是安全的,因此,函数式编程天生就具有跨线程的安全优势,很适合开发并行计算应用。
回调函数设计方法一节里的一段字符串拷贝函数:
void xg_strncpy2(char *pD, char *pS,int nDestSize){
int nLen=strlen(pS)+1;
if(nLen>nDestSize) nLen=nDestSize; //目标地址和拷贝地址取小
memcpy(pD,pS,nLen);
*(pD+nLen-1)='\0';
}
请我喝杯咖啡 
