C++11新标准阶段性学习心得及两个小轮子分享

  突然发现,自己的博客好久都没更新了。
  主要缘由是这段时间忙于学习C++11和Boost的开发,同时公司也有一个小项目正在设计开发之中,所以工作成果都转换成笔记和代码了。
  说来自己学习C++11已经一月有余了,之所以称之为学习而非温习,是感觉C++11和C++98的差异实在是太大了,体现的不仅是小特性的改进,而感觉是一个质的飞跃。V2ex上人家调侃说十几年前自己学了一门语言叫做C++,现在那个语言还是叫C++,只是我已经不认识他了。当初也有个朋友说,如果C++11早一点出来,那么Google也不会推Go语言了,而现在的开发语言的格局也会大有不同,至少说不准如当下Java大行其道作为绝大多数公司服务端开发的主力语言了。无可厚非,服务端的高性能开发的极致非C/C++莫属,所以C++是一门低碳环保的语言,而C++11的右值引用、移动操作等无不在最求性能的极致。
  下面就着这段时间的学习心得体会和耳濡目染,介绍一些关于C++11的新认识。同时,在工作和学习之余,自己也模仿着造了两个小轮子,已经托管到GitHub上面了——虽然简单,也很不专业,但是对C++11的新特性都覆盖了不少,算是在学习中动手、在实践中改进吧。

一、C++11新语言

1.1 C++11

1.1.1 RAII和标准库

  以前学C和C++,在C/C++中的堆内存的动态分配,以往都是教条式的强调malloc/free、new/delete、new[]/delete[]一定要匹配使用!千万不要忘记!而且不能free/delete多次!者虽然不是什么技术问题,但是人毕竟是人,难免不出错。而RAII的技巧,就是利用stack上的临时对象生命期的性质,当临时对象超过作用域的时候析构函数会被自动调用,于是利用程序自动管理的这一特点,可以将资源的释放操作封装在一个临时对象中自动销毁,改变以往的手动申请和释放的模式。在C++11中,几乎绝大多数类型,通过一些技巧或者包装,都可以保证其到达作用域结束的时候被正确的析构掉,而且是异常安全的。所以理论上,通过C++11标准的程序,理想状态下不需要new/delete方式来管理对象了,使用智能指针、智能锁等工具重新组织你的工程,从资源管理的泥淖中解脱出来。
  其次,据说C++新标准中有2/3的篇幅是关于容器和算法库。当然有人一直争论这些容器和算法库不应当作为一个语言的标准这种形式存在,否则语言的标准将会不断的膨胀。但是作为实用主义来说,这些标准容器和标准库的存在可以大大增加开发进度,同时别人调试好的数据结构和算法也更容易开发出稳定的程序来。

1.1.2 Boost库

  前面说过,Boost库算是C++标准的试验田,诸位C++大牛都会把特性想法在这里开发,然后好的东西会被C++标准委员会吸纳进正式标准,比如C++11中智能指针就是从Boost中引入的。但是如陈硕老师所言,Boost库规模庞大,但也不能盲目尽信之,好的东西譬如如智能指针、noncopyable等可以直接用,免去自己制造垃圾轮子,而且可以借鉴取其实现思路和方法,以增长功力。但是也有些库用到的技术晦涩难懂,和实际使用有所脱节,而且可能效率很低。当然这些甄别取舍也是门技术,惭愧自己到达那份功力还路途遥远。
  还有需要注意的是,很多的东西在Boost和C++11标准中都有一份,虽然大体功能相似,但是还是有些差异,比如:std::unique_ptr可以传递deleter函数,但是boost::scoped_ptr却不支持。看个人感觉吧,但是建议不要混用,因为即使std::shared_ptr和boost::shared_ptr等价,但是函数调用参数却不这么认为。

1.1.3 编译速度非常慢

  C++的运行速度不慢,但是由于现在的库中引入了大量的模板类,而模板的解析、实例化、编译等很很需要计算量。一个十来个文件的小项目在笔记本上要干一分多钟,实在忍受不了。网上搜罗的解决方式有:
  (1). PCH预编译头部
  照网上的说法是建立一个头文件xxx.hpp,包含在项目中常用稳定的头文件,然后编译这个头文件生成同名xxx.gch的预编译头结果,后续在项目中包含xxx.hpp就会直接使用xxx.gch预编译结果,从而减少编译时间。我照着做了,没效果,在stackoverflow上面问了好久About g++ not working with pre-compiled headers pch,也没人鸟我。
  不知道是GCC的BUG还是我的姿势不对,后面试试Clang吧。我觉得这种预编译头还是挺有用的,如果有预编译结果会得到优化,如果没有也能正常编译,可惜这里暂时不能用。
  (2). ccache缓冲
  问了罗剑锋,他说ccache应该可以优化编译效率,不过我还没试。

1.2 C++编译器

  C++的编译器,基本是Windows的Visual C++和Linux平台下的g++成了默认编译器的了,但是近年来Clang+LLVM的编译套件在Mac和Linux平台声势很猛。网上搜了一下,主要声音是:
  (1). GCC虽然是GNU社区开源旗舰产品,但是总体对大商业(闭源)公司不见得多友好,据说苹果提了很多Objective-C的特性,结果GNU就是不鸟他们。没办法,信仰不同嘛,再加上程序员总是有些偏执和骨子里面的犟。所以苹果招了个奇才搞出了个Clang,然后以更加宽松的BSD协议分发,这时候谷歌等大公司也乐呵进来了,众人拾材火焰高啊,看样子这是要把GCC扔进垃圾箱的节奏么。
  (2). Clang和LLVM是整个编译器的前段和后端,而GCC是一体的,所以Clang可以更加专注于做好词法和语法分析。怎么感觉老一代的软件都是又大又全,新一代的软件都是段小精悍的形式出现,就像Apache和Nginx一样的案例,Apache集成了大量模块,而Nginx强于异步事件来实现高并发,很多任务反向代理给后端就好。
  (3). 据说Clang编译出错提示比GCC好,尤其是对C++这种复杂的语言。后面我想切换到Clang体验一下,不过很多库默认都是GCC编译打包的,可能会有些不方便。还有人爆料最近阶段,GCC的代码质量大不如前了,这我倒没感受啥,但GCC现在疯狂的刷版本号是搞的哪一壶?
  (4). 用久了总归会审美疲劳的,就像乔帮主当年的拟物图标设计的多么完美生动,后来换成扁平后还是大受欢迎,也不能说谁绝对的好与不好。

二、服务端的信息分发

2.1 工作原理

  C++下的异步开发库最有名的就数boost.asio了,其设计跟epoll以及最常见的libevent不同,为Proactor模式的,意味着在回调函数被调用的时候,IO操作已经完成了,这最典型的Windows完成端口模式。其好处就是,当IO任务特别多的时候,传统的Reactor可能会将任务队列排的很长,整个系统的响应速度变得恶化。
  自己用boost.asio做了一个airobot_msgd的程序。这个程序本来是给自己另外一个程序做的,目的就是将之前的一个程序开启多个运行实例,然后这个前端程序可以把所有的请求分发到后端处理,并将后端的处理结果再返回过来转发给请求客户端。其实这种算是很常见的应用需求,一方面可以增加整个系统的性能,同时也增加了容错性:后台程序只要不全挂完,总能得到响应。
  或许你会问,这个不就是Nginx反向代理可以做的么,而且可以设置负载均衡参数。但是我想说airobot_msgd收到数据后会进行一个初步解析,将相同一个会话转发到后台的同一个实例上处理,并不是HTTP那种完全无状态的请求,后端要求连续的会话信息哦!
  不过,如果重新设计开发这个项目的话,我可能会用MessageQueue来做了。

2.2 实现细节

  这个软件的异步操作是使用的boost.asio。高吞吐量并发的异步IO模型在boost.asio中主要有三种实现方式:an io_service-per-CPU、a single io_service and a thread pool以及stackless coroutines模式。
  (1). an io_service-per-CPU: 算是最简单的吧,每个io_service都在一个CPU上面串行执行,各个io_service没有影响,而每个socket是绑定在一个io_service上面的,基本不需要什么额外的保护了;
  (2). a single io_service and a thread pool: 这种方式在异步读写的时候需要strand来保护,确保其被包装的相同socket回调函数串行化不会被多个线程同时执行(还有待深入考证);
  (3). stackless coroutines: 这种方式在Python等语言中用的很多,称为协程开发,但是感觉C++中用的不是很主流啊。因为C++本来就有高性能的native线程啊。
  由于要根据前端的会话信息进行后端服务分配,所以需要json解析POST的消息体。GitHub上面Json的库之多如牛毛,我选的是json11这个库,就.hpp和.cpp两个文件,很容易代码方式集成到项目中,功能也都够用,而且用最新C++11实现的,RAII所有对象自动析构,可以像普通变量一样使用(同时在这个库中没有发现一个new/delete)。但是这个库有个缺点,就是整形在底层是用double存储的,精度只能支持+/-2^53范围,他们在头文件中说明地很清楚,因为javascript这类语言就支持不了大整形的。但是我觉得uint64_t这种类型还是很有用的,于是自己fork了一份,修改支持64位整形,并用在了项目中。
  后面透过《Linux多线程服务端编程》这本书,代码也修改了一些,比如:使用time_wheel定时清理非活跃连接;服务端可以主动关闭写端,被动关闭整个连接等,随着后面的学习代码可能会持续更新。

2.3 TODO

  (1). 当前对每个链接请求都是先创建connnection,结束后销毁connection,但是反复创建和销毁对象对性能还是有很大的影响的,后续优化会建立一个connection的对象池,新链接到来的时候在对象池中取出然后进行关键参数的初始化,使用完毕后进行敏感数据的处理后,再丢回对象池中;(目前已经实现)
  (2). 当前只是简单的解析了HTTP的请求头部,以及固定的回复头部,后面对HTTP协议有深入了解的话,可以增加对这些头部特性实质性的支持。

三、方便使用的数据库连接池

  之前在进行C开发的时候,就已经写了一个数据库连接池,感觉是很有用的,于是在C++的时候也没能把持住,造了另外一个轮子,项目在aisqlpp

3.1 工作原理

  其实说起来很简单,就是用一个std::map存储链接和对应的占用、空闲、错误信息。在此,MySQL的官方也是有基于C++11和Boost的客户端连接库mysql-connector-cpp,个人算是在这个连接库中对其封装了一下,让其看起来更好用更安全吧。

3.2 实现细节

  (1). 在每个连接中,将stmt、prep_stmt、ResultSet等信息都封装到类中,当每次需要进行新的查询或者操作的时候,都更新这些类变量,所以也不用繁琐的申请和释放这些变量了。显然,这些变量都不是线程安全的,不过任何操作过程中都需要申请这个链接,使用完毕后释放这个链接,这个过程中连接不会分配给别的线程,所以算是线程安全的。在前面的应用模型中,最好是线程池中每个线程固定分配一个链接,可以避免每次申请和释放的繁琐过程了,不过这得我深入确切的了解io_service线程池模型和strand做了什么才部署。
  (2). 其次就是在查询结果的解析中,采用C++11的模板技术,便利的封装了一些查询结果的获取方式,支持单个列多条记录的查询返回和一条记录的多个列返回,让整个查询就像调用一个函数一样那么轻松。如果能用动态模板类将多条记录的多个列结果封装返回就好了,后面尝试看看。
  (3). 然后,还根据RAII的原理,提供了一个request_scoped_conn接口,这个获取的的connection在离开其作用域的时候会自动释放该连接,使用起来更加方便了。

后言

  最后,昨天在看陈硕的《Linux 多线程服务器端编程》的时候,遇到一段摘抄,是孟岩老师(不认识,不过陈硕引用了很多他的东西)的《快速掌握一个语言最常用的50%》,大家可以看看原文吧。
  我的个人感觉呢,对于核心业务,确实不能让半瓢水的程序员匆忙上阵,否则埋的坑多了,对后续系统的维护都是灾难性的。而对于初学者,应该一定程度上多看多造轮子。如果一说到什么项目,总是想到这个库那个库的,直接拿来不勤于探究的话,那么你就充当个系统集成工程师了,技术得不到锻炼和成长。同时也不建议只看书不动手, 以为把书翻烂了知识点烂熟于胸,写起代码来能信手拈来。其实我的建议是一边写程序一边看书,当遇到新的知识点时候,看看自己的程序能否有改进,能否更优雅的实现,让程序和你的知识一样慢慢玩备起来。当然你的程序如果最终有条件线上测试,就更好了。

本文完!