对C的新感觉

      加入公司两周了,进公司的第一个工作是为公司调试一个叫做libevent的库。以前总是用C++,很少用C。这回第一个任务就是要去改一个C写的库,起初感觉很不适应。

   以前用C++写程序,感觉就像在吃剥了皮,抽了芯的莲子。虽然之前剥皮和抽芯的过程很麻烦,但是之后吃起来就很顺畅。用C++做项目的关键是划分对象。对象的划分决定了对象之间的关系;决定了信号在对象之间流动的流程;决定了程序的健壮性;决定了程序的可扩展性……决定了除本质上的运行流程之外的其他任何特性。尽管如此,我们大可不必因此而畏首畏脚。东西做多了,自然就知道哪些情况应该如何应对。如果遇到未知的情况,就按照相似的方法进行分析,就算走错了,也可以再改过来。

    这两周一直在改C写的libevent库(see libevent - an event notification library),Boss的要求就是1、不能改库本身的构架,对库的更改要尽量通过自带的所有测试用例(尽量不要去更改测试用例)。2、修复库在Windows平台下的漏洞,例如如果connect没有成功,消息循环不会给调用者发送任何出错消息,从而使程序死在那里。3、为libevent加入IOCP,用IOCP模型代替库本身自带的select模型(see 集成 IOCPLibevent)。

    看过libevent的代码之后,我对C有了全新的认识。之前在某家公司面试时,面试官对我大谈C的优点,说系统底层绝对是用C比用C++强得多,我打心底里不以为然。现在虽然仍然不赞同,不过C的确比C++有很多优势。

   首先,C比C++的确要简洁许多,无论细节和大构架。原先以为C++细节复杂,但大构架比C简单,其实不然。用C写这种消息库关键是回调函数(CallbackFunctions)。调用者只需要写一个函数即可,而不必写一个class,还得从某一接口继承。其次就是handle。libevent中的struct event算作一个handle(并不是所有的handle都必须是void*,还可以是想event这样的结构体),围绕这个handle有一系列的操作。libevent中有event_add和event_set这两个关键操作。再次就是Custom DataPointer。libevent为每一个event绑定一个用户指针,指向一个用户数据。用户可以通过这个数据来判别这个event是因什么而产生的。联想到C++Builder中的许多控件,例如TTreeView和TListView也有这么一个成员(Data成员),可以为每一个子对象绑定一个用户数据,成为子对象的一个扩展属性。用C++来实现这些构架都比C复杂得多,弄不好还会把使用者的脑神经给弄打结了。C的直来直去的特性的确是其最大的优点。

  其次,C比C++快。原先以为好的C++程序绝对是和C的速度一样的,其实又不然。在libevent中有大量的函数指针作为struct的成员。当某一类对象具有同样的操作,但某些对象有这些操作没有那些操作,其他对象恰恰相反时,用C++实现就需要继承,判断函数返回值。虚函数是必须要有实现的,有实现就必然会被调用。而C只需要判断指针是否为空!无需调用该函数。仅仅由此带来的速度优势是C++无法比拟的。类似的情况还有很多。至少C中通过函数指针调用函数只需要取一次地址即可,而C++的虚函数至少要取2次地址。

    但是,C程序的扩展性不强。这一点在我更改libevent的过程中一步一步体现出来。

    iunknown提供的IOCP解决方案(see 集成 IOCPLibevent)是不完全的。准确的说,该方案是以EventSelect模型为主,以IOCP为辅的构架。iunknown的解决方案为accept设计了专门的EventSelect消息链,而所有recv和send操作则由IOCP模型负责触发。这就导致一个设计上的混乱:libevent设计只有一个消息循环,而iunknown的构架中有两个:一个需要WaitForMultipleEvent来处理accept消息,另一个需要用GetQueuedCompletionStatus来处理IOCP结果。而作者先以timeout=0调用后者,也就是说只是试一下IOCP是否有完成消息,然后以timeout=INFINITE调用前者。这就是为什么我说这不是一个完全的IOCP模型,只是一个以EventSelect为主搭配以IOCP的模型。而且该模型实现中没有考虑connect消息,也就是说,必须使用同步connect,这是开发组不能接受的。

    后来在Boss的同意下,我决定自行实现完全的IOCP的实现,所有的消息通过GetQueuedCompletionStatus来触发。这样的设计符合libevent的总体构架。然而一个很郁闷的问题摆在了眼前:IOCP从本质上与libevent不兼容!

  libevent的思路是这样的:socket之上有四种常用操作:connect、accept、recv、send,在用户调用这四种常用操作之前,先由libevent来“测试”一下,看socket是否具备调用某一操作的前提,如果符合,则触发用户定义的回调函数。在此情况下,用户调用这四操作(非阻塞同步模式下)则必然会立刻返回,而不会因为条件不成熟而阻塞在那里(同步模式下如果没有数据,recv一个阻塞socket也会导致线程等待),或者recv返回0导致无意义的操作(条件成熟时返回0是因为对方正常关闭连接,条件不成熟时因为没有数据)。

   而IOCP的思路是这样的:不管条件是否成熟,用户先调用那四个操作再说,调用会立刻返回,但不立刻完成,而要用GetQueuedCompletionStatus来等待调用的结果。

  因此libevent是先奏后斩,而IOCP则是先斩后奏。如此思维上的巨大差异是不可调和的。拿accept来说,libevent的先奏后斩意味着accept得到的客户socket可以在回调函数中创建并获得。而IOCP的先斩后奏则需要先创建一个socket,等到完成时,改socket才是一个有效的已连接的socket。那么libevent有什么机制可以让socket创建之后一直保留到其有效时通知调用者呢?貌似没有。如果你要更改struct event这个类似handle的结构,那可是伤筋动骨的改动。这个结构在libevent的核心代码中无数次使用!

    当然,最后这个问题在多方调节下算是圆满解决。但是暴露出一个问题:C虽然简单,虽然快,但到关键时候就很痛苦。很难有极具扩展性的设计。当别人拿到你写的C库的时候,你是否能够保证被人在不更改你原有代码的情况下可以随心所欲的扩展其功能?

    设计模式中很多东西无法适用于C。就拿这个例子来说,如果我来用C++设计libevent,象structevent这样由用户来创建和维护的Handle会被设计成一个由ClassFactory来创建的对象,该对象可以由用户自行继承和扩展,从而可以按照需要加入任何附加数据,并让这些附加数据随着Handle在添加、循环、触发的过程中流动,最后回到调用者那里。如果用C来实现就非常复杂,得不偿失。

   不过通过第一次详细阅读一个用C写的库还是很有收获。这并不是说我将抛弃C++而投奔C,而是从C中汲取有益的经验为以后写出更好的C++程序,也即取长补短。

   收获之一就是指针的转换。用面向对象的思维,一个指针从父类直接转换为子类是不安全的。要用dynamic_cast或者借鉴COM的QueryInterface。以前我也这么做。钻研这些细节多了,就会导致对细节很在意,以为只有保证每一个细节的“绝对安全”,一个程序才是安全的,这往往降低了整个系统的速度。其实C里面充斥着这种直接的转换,这是C直来直去的特点,也是C快速的根源之一。如果构架能够设计的足够好的话,指针从父类到子类的直接转换也并不意味这不安全,只要你能保证这里的指针虽然由框架决定了只能是父类指针,但是一定指向的是调用者提供的子类指针,那么这种转换就是安全的。这就好比设计一个金字塔,我们不需要每一块石头都与周围的石头严丝合缝,但是只要我们设计的结构是安全的,无需特殊的粘合措施,仅靠石头本身的自重,金字塔就可以屹立几千年不到,哪怕它是几千年前落后科技的产物;也好比是设计一个木头房子,好的隼铆结构就可以保证木头房子在狂风中纹丝不动,虽然没有用一颗钉来加固,哪怕是用了几百年的老房子呢?

    因此细节固然重要,大构架更重要。Vista安全性不容置疑,可为什么这么慢?是不是有这个方面的原因呢?

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值