《0 bug:C/C++商用工程之道》读书笔记

书名:《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';
}
请我喝杯咖啡