写huasio库的心得

寒假里我了解了一下node.js,感觉它的全异步设计很好玩,于是就系统地学习了一下有关异步编程的知识,
同时大概了解了一下node.js的底层实现————libuv的设计。libuv是个基于事件驱动模型的库,异步部分使用libeio编写。
之前我就了解过一些linux平台上异步IO的情况,可惜linux原生的aio很不完善,glibc里面的异步库bug也很多。出
于好奇和好玩,我打算参考一下libeio自己实现一个。

仓促的编写

从开始构思到动手写之间大概有两天时间。开始写之前我先大概看了一下libeio的实现
libeio是用C写的,使用了大量的宏,可读性不是很好,但是大体框架还是能看明白的。libeio是用线程池+IO多路复用实现的,我的想法也很类似。但是libeio的API设计我不是很喜欢。libeio的API设计是由IO发起者调用一个轮询函数来统一执行注册好的回调。我比较喜欢node的异步写法,就是在调用异步函数的时候直接传入回调,之后自动执行,也就是IO的发起者和终止者不一样,所以这其实并不是个好的设计……

大概看了看之后,我的脑子里其实只有一个线程池+epoll的大概思路,其他的诸如IO返回值之类的问题都没有考虑到,应该说是非常仓促。昨天晚上开始写,写了仨小时,由于前期的具体构思太少,所以编写过程中对很多细节纠结了很久,API也改来改去的。比如我都把as_write函数写完了才发现没法把write函数的返回值传回去。后来想了一下用了std::promise和std::future在IO线程和主线程之间传递结果。再比如我写完了as_write和as_read才发现这俩函数基本是一致的,于是又把API改成了一个as_reg。

以后在动手之前有几个问题一定要确定下来:API的设计,包括参数,返回值;跨线程的通信和同步。第一点尤为关键,因为以后在多人协作的时候如果来回改接口后果会非常严重。

灾难一样的debug过程

代码其实很短,才一百来行,我以为debug的过程会很容易。结果光编译错误就排了四五十个。造成问题的本质还是在于我前期的构思太缺乏,写的过程中来回改,改还改不完全,经常是定义的地方改了结果声明的地方没改。在已有的代码上进行修改本来就容易出问题。

C++11多线程程序的编译问题我又遇到了,一开始运行程序的时候一直弹system_error,我又google了一段时间才解决了这个问题,这次记下来不要再忘了。
g++ -std=c++11 -lpthread,顺序都不能错,这样我才用g++4.9.2成功编译了。

编译问题解决之后还是不断出问题,先是epoll一直报错,还好我在程序里每个有错误返回码的地方都进行了判断,要不然都不知道是哪出的问题。我在用epoll监视普通文件的文件描述符时候一直errno=1,苦恼了半天最后查了查TLPI,结果上面说epoll不能监视普通文件的描述符,当时我就跪了。不过我对这个库的主要期望用途是网络编程,所以不能监视普通文件也无所谓,就没有进行大改,只是把demo改了一下。

在这些问题解决之后多线程的部分又出了问题,std::thread没有退出函数,我之前也完全没考虑这事,结果线程池就结束不了了。当时我也是特别烦躁,因为debug了俩多小时了,后来就给妈妈打了个电话,然后洗洗睡了。

第二天上午我又考虑了一下,然后回来进行了大幅度重构,结果重构部分又出了问题,因为我有部分函数是直接复制粘贴的,导致其中的变量作用域出了问题。std::thread的退出问题后来我用一个flag变量解决了,一旦flag==true就弹出事件循环。这次debug花了一个小时,终于跑起来了。

总结

这次编写过程可以说是一个完美的反面教材。首先,在构思阶段考虑非常不成熟,不是只要想清了原理就能写的。除了上面说的API和跨线程问题之外,还要考虑程序的开始、结束部分。中间的运行过程可能想清楚了,但调用部分和终止部分的边界很容易形成盲区。
其次,对不熟悉的函数千万要慎用,比如我这次的epoll。在使用之前一定要先写个test大概熟悉一下。再次,提前的优化往往是万恶之源,我这次有很多地方都是写着写着发现了一点点能优化的地方然后就改进,然后改出问题了。在重构的时候也尽量不要复制粘贴。修改的时候声明、实现要记全,一起改。最后,在写一个函数之前就要考虑一下耦合和内聚怎么样,可重用性怎么样,尽量不要写了一大坨之后再来回拆分修改。

同时这次还学习了一下commit的规范,大的commit要写成issue,然后再commit里写”issue #xxx, author xxx”这样,简单的修改就先以操作打头,比如”Fix”, “Mod”, “Rem”, “Add”等等,然后再后面尽量简略地写上原因什么的。

希望下次写东西的时候不要再这么荒唐了。

C++