陈硕先生是位资深的C++工程师,其对C++领域知识的掌握,令我叹服。
他的博客在这里: http://blog.csdn.net/Solstice
日前,陈先生作为我的新书《0 bug - C/C++商用工程之道》的读者,在豆瓣网对本书做出了客观的评价,无论是批评还是认同,都令我非常感动,我也做了认真的回复。
我感觉,我们之间的一些认知差异,其实主要还是根据不同的开发角度理解不同,大家在很多理念上还是有很大的共识,我们之间的讨论,对于初学C和C++的朋友,特别是嵌入式,服务器开发类别的,有多线程多任务开发需求的程序员朋友,有一定借鉴作用。
因此,在征得陈先生同意的前提下,我把我们沟通的细节拷贝到这里,供大家参考。
陈先生原文:
大致读了一遍,说几点体会。
这本书前半部分主要讲编程风格,后半部分介绍了作者自己多年积累的一些程序库。
编程风格见仁见智,我喜欢作者只用 for (int i = 0; i < n; ++i) 循环(《程序设计实践》也是这么提倡的),但不喜欢像他那样使用 goto 和宏。在 C++ 里,goto 不只是跳过几行语句跳出几层嵌套那么简单,还涉及对象的初始化,而 goto 不能跨越初始化,编译器会报错。在 C 语言里使用 goto 或许还可以接受,C++ 里不行。
这本书把代码用无衬线非等宽字体(大概是 Arial 之类)印在灰色底纹上,读起来很费眼。我只认真读了第 6 章《锁》的代码,因为我对多线程编程比较熟悉。后面几章的内存池和队列等没有细看,只大致浏览了一下。通读整本书的代码,有几点我很喜欢,第一是没有用异常,第二是没有用继承(也就没有虚函数、设计模式这些东西),第三是只出现过一个类模板,代码见 http://blog.csdn.net/tonyxiaohome/archive/2010/01/03/5124521.aspx
也有几点我不喜欢,整本书的代码基本上都是披着 C++ 外衣(马甲)的 C 代码,作者多次先用 C 语言实现某个功能,再用 C++ 简单封装一下。整体代码风格有 90 年代中期用 Borland C++ 开发的 C/C++ 程序的感觉:几乎没有见到 C++ 标准库的使用,只使用了少量 C 的标准库(strcpy/memcpy/vsnprintf 之类,从第 8.3.5 节看来 memmove 似乎被遗忘了),喜欢自己写一套,大量重新发明轮子。C++ 语言的使用基本停留在十年前的水准,与当前 C++ 社区推崇的实践风格(我不是指模板元编程那一套,而是指 RAII)相去较远,连最基本的成员初始化列表和 const 修饰符都很少用到。这样的代码风格,在我们组肯定会被毙掉的。
具体说说第 6 章《锁》。作者自己实现了一个读写锁,并命名为“单写多读锁”。说实话我很佩服作者的聪明和勇气。另外我很好奇——除了作者本人和他的团队,谁敢把这段代码用到产品中。如果这段代码是正确的,那么它的效率是否比操作系统提供的读写锁(据我所知,pthreads 和 Windows Vista/7 直接提供了读写锁 API)更好?如果它的效率比系统的读写锁更好,如何证明它是正确的,会不会遗漏了什么边界条件或 race condition 没有考虑?这些是我的疑虑。
我的体会是,搞多线程编程如履薄冰,千万别自己发明东西,那将几乎肯定是错的。不说别的,单是一个 Singleton 模式的线程安全实现就能难倒很多人。一度人们认为 Double checked locking 是王道,兼顾了效率与正确性。后来有神牛指出由于乱序执行的影响,DCL 是靠不住的。(这个又让我想起了 SQL 注入,十年前用字符串拼接出 SQL 语句是 Web 开发的通行做法,直到有一天有人利用这个漏洞越权获得并修改网站数据,人们才幡然醒悟,赶紧修补。)Java 开发者还算幸运,可以借助内部静态类的装载来实现。C++ 就比较惨,要么次次锁,要么 eager initialize、或者动用 memory barrier 这样的大杀器( http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf )。接下来 Java 5 修订了内存模型,并增强了 volatile 的语义,这下 DCL (with volatile) 又是安全的了。然而 C++ 的内存模型还在修订中,C++ 的 volatile 目前还不能(将来也难说)保证 DCL 的正确性(只在 VS2005+ 上有效)。
举这个例子,是想说明编写线程安全的代码(遑论实现线程同步原语)是件多么困难的事情。开发者需要深入理解多 CPU 下的内存模型、乱序、cache 一致性与 memory barrier、原子操作、各种常见陷阱等等,才不会重蹈覆辙。作为一般开发人员——或者如作者所说,商业程序员——最好使用成熟的经过大量人群反复验证的库和 idioms,而不要试图自己发明轮子,特别是这种极不容易造好的轮子。如果形势所迫非造不可,比如当前系统没有直接提供读写锁(Windows XP 及以下就没有),而项目又要用到,那么移植一个现有的实现(无论是 pthreads 或者 Java 或者其他开源项目比如 ACE)或许是个更好的思路,而不是靠自己的聪明才智去发明读写锁算法。
这本书第 6 章用互斥器实现了线程安全的 CMInt 和 CMBool 这两个类。我认为这完全没必要,因为用原子操作 (Windows 下是 _InterlockedIncrement 等) 就能达到同样的效果,而且效率只会更高。另一方面,更重要的是,CMInt 和 CMBool 由于包含了互斥器,那么拷贝构造和赋值操作符应该是被禁掉的,否则两个对象可能意外地共享同一个锁,这会导致难以预料的行为。作者通篇似乎没有考虑拷贝构造和赋值操作符的存在,比如书中的 CMutexLock 竟然是可以拷贝构造和赋值的,这直接会导致多重销毁。C++ 代码只写普通构造函数和析构函数而忽视 copy-ctor 和 assignment,这恐怕很难算是合格的 C++ 程序员(我相信作者是个合格的 C 程序员),毕竟这是《Effective C++》上反复强调的内容。
作者在第 6.1.4.6 节提到在析构函数里额外做一次加锁和解锁,防止程序在多线程下崩溃,这更是错误的,因为对象的生与死不能由该对象自身拥有的互斥器来保护。这个问题很深,但解决起来并不费劲。2009 年 12 月的上海C++技术大会上有一场《当析构函数遇到多线程》的主题演讲,将来有空我会把演讲稿整理成文放到博客上。
这本书或许能从一个侧面反映国内 C++ 开发的大体水平。C++ 照作者这么用,固然不符合我的审美和我们团队的性能要求,但也不妨碍做出质量性能合格、能卖出去的软件。特别是最后几章谈到抓内存泄漏和 Sockets 泄漏,虽说办法土,倒也是挺奏效的。说实话,我挺佩服作者在缺乏工具的情况下自己设法制造工具解决问题的能力。
我不后悔买了这本书。总的来说,这本书值得去读,可以以很低的代价了解别人和别的公司在工作中是怎么做的。这些做法够不够好,是不是能更好,自己遇到了如何解决?这又能引发思考,并提供了很多讨论话题。
这本书前半部分主要讲编程风格,后半部分介绍了作者自己多年积累的一些程序库。
编程风格见仁见智,我喜欢作者只用 for (int i = 0; i < n; ++i) 循环(《程序设计实践》也是这么提倡的),但不喜欢像他那样使用 goto 和宏。在 C++ 里,goto 不只是跳过几行语句跳出几层嵌套那么简单,还涉及对象的初始化,而 goto 不能跨越初始化,编译器会报错。在 C 语言里使用 goto 或许还可以接受,C++ 里不行。
这本书把代码用无衬线非等宽字体(大概是 Arial 之类)印在灰色底纹上,读起来很费眼。我只认真读了第 6 章《锁》的代码,因为我对多线程编程比较熟悉。后面几章的内存池和队列等没有细看,只大致浏览了一下。通读整本书的代码,有几点我很喜欢,第一是没有用异常,第二是没有用继承(也就没有虚函数、设计模式这些东西),第三是只出现过一个类模板,代码见 http://blog.csdn.net/tonyxiaohome/archive/2010/01/03/5124521.aspx
也有几点我不喜欢,整本书的代码基本上都是披着 C++ 外衣(马甲)的 C 代码,作者多次先用 C 语言实现某个功能,再用 C++ 简单封装一下。整体代码风格有 90 年代中期用 Borland C++ 开发的 C/C++ 程序的感觉:几乎没有见到 C++ 标准库的使用,只使用了少量 C 的标准库(strcpy/memcpy/vsnprintf 之类,从第 8.3.5 节看来 memmove 似乎被遗忘了),喜欢自己写一套,大量重新发明轮子。C++ 语言的使用基本停留在十年前的水准,与当前 C++ 社区推崇的实践风格(我不是指模板元编程那一套,而是指 RAII)相去较远,连最基本的成员初始化列表和 const 修饰符都很少用到。这样的代码风格,在我们组肯定会被毙掉的。
具体说说第 6 章《锁》。作者自己实现了一个读写锁,并命名为“单写多读锁”。说实话我很佩服作者的聪明和勇气。另外我很好奇——除了作者本人和他的团队,谁敢把这段代码用到产品中。如果这段代码是正确的,那么它的效率是否比操作系统提供的读写锁(据我所知,pthreads 和 Windows Vista/7 直接提供了读写锁 API)更好?如果它的效率比系统的读写锁更好,如何证明它是正确的,会不会遗漏了什么边界条件或 race condition 没有考虑?这些是我的疑虑。
我的体会是,搞多线程编程如履薄冰,千万别自己发明东西,那将几乎肯定是错的。不说别的,单是一个 Singleton 模式的线程安全实现就能难倒很多人。一度人们认为 Double checked locking 是王道,兼顾了效率与正确性。后来有神牛指出由于乱序执行的影响,DCL 是靠不住的。(这个又让我想起了 SQL 注入,十年前用字符串拼接出 SQL 语句是 Web 开发的通行做法,直到有一天有人利用这个漏洞越权获得并修改网站数据,人们才幡然醒悟,赶紧修补。)Java 开发者还算幸运,可以借助内部静态类的装载来实现。C++ 就比较惨,要么次次锁,要么 eager initialize、或者动用 memory barrier 这样的大杀器( http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf )。接下来 Java 5 修订了内存模型,并增强了 volatile 的语义,这下 DCL (with volatile) 又是安全的了。然而 C++ 的内存模型还在修订中,C++ 的 volatile 目前还不能(将来也难说)保证 DCL 的正确性(只在 VS2005+ 上有效)。
举这个例子,是想说明编写线程安全的代码(遑论实现线程同步原语)是件多么困难的事情。开发者需要深入理解多 CPU 下的内存模型、乱序、cache 一致性与 memory barrier、原子操作、各种常见陷阱等等,才不会重蹈覆辙。作为一般开发人员——或者如作者所说,商业程序员——最好使用成熟的经过大量人群反复验证的库和 idioms,而不要试图自己发明轮子,特别是这种极不容易造好的轮子。如果形势所迫非造不可,比如当前系统没有直接提供读写锁(Windows XP 及以下就没有),而项目又要用到,那么移植一个现有的实现(无论是 pthreads 或者 Java 或者其他开源项目比如 ACE)或许是个更好的思路,而不是靠自己的聪明才智去发明读写锁算法。
这本书第 6 章用互斥器实现了线程安全的 CMInt 和 CMBool 这两个类。我认为这完全没必要,因为用原子操作 (Windows 下是 _InterlockedIncrement 等) 就能达到同样的效果,而且效率只会更高。另一方面,更重要的是,CMInt 和 CMBool 由于包含了互斥器,那么拷贝构造和赋值操作符应该是被禁掉的,否则两个对象可能意外地共享同一个锁,这会导致难以预料的行为。作者通篇似乎没有考虑拷贝构造和赋值操作符的存在,比如书中的 CMutexLock 竟然是可以拷贝构造和赋值的,这直接会导致多重销毁。C++ 代码只写普通构造函数和析构函数而忽视 copy-ctor 和 assignment,这恐怕很难算是合格的 C++ 程序员(我相信作者是个合格的 C 程序员),毕竟这是《Effective C++》上反复强调的内容。
作者在第 6.1.4.6 节提到在析构函数里额外做一次加锁和解锁,防止程序在多线程下崩溃,这更是错误的,因为对象的生与死不能由该对象自身拥有的互斥器来保护。这个问题很深,但解决起来并不费劲。2009 年 12 月的上海C++技术大会上有一场《当析构函数遇到多线程》的主题演讲,将来有空我会把演讲稿整理成文放到博客上。
这本书或许能从一个侧面反映国内 C++ 开发的大体水平。C++ 照作者这么用,固然不符合我的审美和我们团队的性能要求,但也不妨碍做出质量性能合格、能卖出去的软件。特别是最后几章谈到抓内存泄漏和 Sockets 泄漏,虽说办法土,倒也是挺奏效的。说实话,我挺佩服作者在缺乏工具的情况下自己设法制造工具解决问题的能力。
我不后悔买了这本书。总的来说,这本书值得去读,可以以很低的代价了解别人和别的公司在工作中是怎么做的。这些做法够不够好,是不是能更好,自己遇到了如何解决?这又能引发思考,并提供了很多讨论话题。
我的回复:
嗯,我也说几句。
几位大牛确实一针见血,说出了我很多缺点和弱点,在此表示感谢。
几位大牛确实一针见血,说出了我很多缺点和弱点,在此表示感谢。
嗯,我还是申辩几句哈。
没有完全使用C++,固然有我本人水平不够的影子,呵呵,我自我评价,我是标准的山寨C++程序员,嘿嘿。
但,中间大牛们百思不得其解的一个奇怪写法,我这里解释一下。
为什么不去直接用C++的类封装,而是费尽心机写了一个结构体封装类成员数据,然后写一堆C函数,然后再用类来封装。这是有原因,原因呢,书里面也写了。
关键是为了预防内存碎片。
C和C++的动态内存分配机制,有个问题,长期,大量申请和释放后,会有内存碎片,严重的会导致服务器挂死,而我开发的很多工程,都有7*24小时工作需求,因此,内存碎片普通程序员可能都感觉不到,却成了我必须解决的难题。
因此我做了个内存池,主要是基于重用理念,仿造STL的内存池构建方法,建立自己的多线程安全内存池。
但是,这个内存池有个缺点,只能提供malloc和free服务,不具备new和delete操作,我编译原理学得不好,确实不知道怎么做,呵呵。
这就说明,我的内存池,只能为纯数据区块服务,无法应对动态对象的申请和释放。
这就说明,我的内存池,只能为纯数据区块服务,无法应对动态对象的申请和释放。
但是,C++的对象在大规模工程组织中的便利性我又舍不得放弃,没办法,我就做了个奇奇怪怪的方法。
对于某些高频申请和释放,且长度不等的应用需求,比如,传输服务器常见的动态哈希表,用于目标地址,或者一些报文的快速存放和取出的工具,我习惯于这么做。
1、类里面手工区别成员变量和成员函数,先用C的struct实现成员变量组,然后用纯C方式提供访问逻辑。
2、用类封装,封装时,内部所有成员变量利用一个struct指针来表示,这时候,这个struct可以在内存池上分配。
我们知道,通常一个对象的绝对尺寸,主要还是取决与内部的数据,函数表占不了多少字节的,几十个字节顶天了,当占主要尺寸的内部数据区使用内存池管理,规避了内存碎片之后,几十个字节的对象,一般说来,随便new和delete,都不会再有内存碎片的风险了。
对于某些高频申请和释放,且长度不等的应用需求,比如,传输服务器常见的动态哈希表,用于目标地址,或者一些报文的快速存放和取出的工具,我习惯于这么做。
1、类里面手工区别成员变量和成员函数,先用C的struct实现成员变量组,然后用纯C方式提供访问逻辑。
2、用类封装,封装时,内部所有成员变量利用一个struct指针来表示,这时候,这个struct可以在内存池上分配。
我们知道,通常一个对象的绝对尺寸,主要还是取决与内部的数据,函数表占不了多少字节的,几十个字节顶天了,当占主要尺寸的内部数据区使用内存池管理,规避了内存碎片之后,几十个字节的对象,一般说来,随便new和delete,都不会再有内存碎片的风险了。
这实际上是根据实际使用需求,强行拆裂C++对象中数据与方法的集合,使之分别从不同的内存区申请内存,以实现尽可能的运行安全性,避免内存碎片的影响。当然,这里面还有个用以,我做工程的环境,很多时候不一定是C++环境,有可能某些时候,就碰到纯C的环境,这么开发的话,很多基本的工具,我可以同时提供纯C和C++两套api,调用更加方便。
主要目的就是这个,不知道我讲清楚了没有。
这是实战出来的经验,因为过去很多次,我的服务器就是因为这个原因挂掉的,而且,很不好找原因。
这是实战出来的经验,因为过去很多次,我的服务器就是因为这个原因挂掉的,而且,很不好找原因。
嗯,再说一点,原子锁我研究过,不过,我发现,原子锁和操作系统关系太密切,我要求我的库是跨平台的,嗯,这个就不劳大家批评了,这是我的设计需求。
因此,我一般不太喜欢沾操作系统太多的东西,所以,我在锁上面又做了一层封装,实现了跨平台的安全调用机制,至少从我的工作中,这种努力时值得的,因为我通常同时写一个传输协议的客户端和服务器端,而客户端使用Windows平台,服务器端是Linux平台,二者还都有可能是arm linux平台。
这么写,看似效率不高,很怪异,但总好过我同时维护几套库。大家说是不是?
因此,我一般不太喜欢沾操作系统太多的东西,所以,我在锁上面又做了一层封装,实现了跨平台的安全调用机制,至少从我的工作中,这种努力时值得的,因为我通常同时写一个传输协议的客户端和服务器端,而客户端使用Windows平台,服务器端是Linux平台,二者还都有可能是arm linux平台。
这么写,看似效率不高,很怪异,但总好过我同时维护几套库。大家说是不是?
嗯,析构函数内无意义的加锁和解锁,我是作为一个小经验分享的,叫做有它更好,没有也无所谓。
近年来,我使用标准成员函数定名的方式,以Start和Stop作为一个包含线程的类的显示起停标志,并且使用防止重入锁规避重入错误,已经是很成熟的编程手法。
目前我开发时,析构函数习惯性调用Stop,确保本类所属所有线程均已经安全退出再释放,其实已经足以保证安全性了,嗯,书上的这个无意义加锁和解锁,其实在很多时候没必要。
我提出这个经验值,主要是考虑到,有些纯工具函数类,可能被n个线程调用,如果程序员不小心,可能会在析构对象时,会导致还有线程没有退出,在访问这个对象,导致挂死。
由于这种情况往往多发生在程序退出时,其实就是有个时间差,也许由于程序员不小心,会有一些线程多活几个周期,多碰几次这个对象,这个,我能避免,但不是所有的程序员都能避免。
因此,我介绍这个经验,就是尽最大可能,帮助程序员在没有控制好的时候,能规避一点,就规避一点。
我也同意,这不是解决问题正确之道,但,有时候能救命,呵呵。
最后我还是要说,我仅仅是一个普通程序员,书中是我自己的一点实战经验,理论可能不一定全对,不过,几乎每个知识点,都是经过bug总结出来的,背后,往往都是十天半个月的加班,痛定思痛,总结一点,呵呵,说经验,都还不如说教训比较准确。
我从来没有认为自己有能力代表国内程序员的水平,我甚至连我们公司都代表不了,我仅仅代表我自己。
最后,还是谢谢各位大牛的关注,如果有问题,欢迎继续讨论。
近年来,我使用标准成员函数定名的方式,以Start和Stop作为一个包含线程的类的显示起停标志,并且使用防止重入锁规避重入错误,已经是很成熟的编程手法。
目前我开发时,析构函数习惯性调用Stop,确保本类所属所有线程均已经安全退出再释放,其实已经足以保证安全性了,嗯,书上的这个无意义加锁和解锁,其实在很多时候没必要。
我提出这个经验值,主要是考虑到,有些纯工具函数类,可能被n个线程调用,如果程序员不小心,可能会在析构对象时,会导致还有线程没有退出,在访问这个对象,导致挂死。
由于这种情况往往多发生在程序退出时,其实就是有个时间差,也许由于程序员不小心,会有一些线程多活几个周期,多碰几次这个对象,这个,我能避免,但不是所有的程序员都能避免。
因此,我介绍这个经验,就是尽最大可能,帮助程序员在没有控制好的时候,能规避一点,就规避一点。
我也同意,这不是解决问题正确之道,但,有时候能救命,呵呵。
最后我还是要说,我仅仅是一个普通程序员,书中是我自己的一点实战经验,理论可能不一定全对,不过,几乎每个知识点,都是经过bug总结出来的,背后,往往都是十天半个月的加班,痛定思痛,总结一点,呵呵,说经验,都还不如说教训比较准确。
我从来没有认为自己有能力代表国内程序员的水平,我甚至连我们公司都代表不了,我仅仅代表我自己。
最后,还是谢谢各位大牛的关注,如果有问题,欢迎继续讨论。
补充一点,goto的使用见仁见智,我坚持我的看法。
在目前并行开发的环境下,很多时候,我们会碰到二元动作。
malloc-free
new-delete
lock-unlock
。。。
尤其是由于锁的加入,二元动作嵌套时,是不允许跨大括号跳出的,必须层级跳出,即嵌套多少层,就要跳跃多少次。这些都带来了编程的麻烦。
因此,我有个总结,多任务开发环境下,对于跳转的精准控制要求到了一个变态的地步。
传统结构化程序设计,由于起源比较早,提出的很多理念,是基于单任务开发环境下总结的,对于多任务环境,不尽适用。这个goto我认为就是一例。
结构化程序设计,强调实用break和continue来代替goto,但由于这二者都没有能力精确标定落点,开发中起到的效果,很多时候还不如goto。
在目前并行开发的环境下,很多时候,我们会碰到二元动作。
malloc-free
new-delete
lock-unlock
。。。
尤其是由于锁的加入,二元动作嵌套时,是不允许跨大括号跳出的,必须层级跳出,即嵌套多少层,就要跳跃多少次。这些都带来了编程的麻烦。
因此,我有个总结,多任务开发环境下,对于跳转的精准控制要求到了一个变态的地步。
传统结构化程序设计,由于起源比较早,提出的很多理念,是基于单任务开发环境下总结的,对于多任务环境,不尽适用。这个goto我认为就是一例。
结构化程序设计,强调实用break和continue来代替goto,但由于这二者都没有能力精确标定落点,开发中起到的效果,很多时候还不如goto。
所以,我坚持,goto不是不能用,但是不要乱用。
我书中讲过上述道理的。
嗯,宏是个人习惯,我书中强调,我既不说宏好,也不说不好,请各位读者自己选用。
不知道我解释清楚没有?
我书中讲过上述道理的。
嗯,宏是个人习惯,我书中强调,我既不说宏好,也不说不好,请各位读者自己选用。
不知道我解释清楚没有?
陈先生email回复:
我已经拜读了,您表述得很清楚,我就不在那儿回复了。那个帖子已经够长,随时有偏题的可能。
说实话,在网络论坛上你一言我一语地来回发帖,不如在email里慢慢说个透。每个人的背景和关
注点不尽相同,思考的角度不同,同样的问题选用的解决方案自然也不同。
体到您回复中提到“这个内存池有个缺点,只能提供malloc和free服务,不具备new和delete操作”,我
知道两个做法,可供参考。
第一个是在 class 里重载 operator new / operator delete,这个我不推荐,因为这等于隐藏了“对象是从
内存池分配”这一事实。
第二个办法是用 placement new ,比如(代码随手写的,可能有错)
这是内存池
class MemoryPool : boost::noncopyable
{
public:
void* alloc(size_t length); // 相当于 malloc,返回的地址是充分对齐的
void dealloc(void*); // 相当于 free
};
我有一个 class,希望在池上创建
class BigInt
{
};
BigInt* p = new BigInt; // 这肯定是不行的,用不到内存池
借助 placement new
void* memory = memoryPool.alloc(sizeof(BigInt)); // 先从池上分配一块内存
BigInt* p = new (memory) BigInt; // 再在这块内存上创建对象
释放的时候不能直接 delete p,会 core dump,需要:
p->~BigInt(); // 手工调用析构函数
memoryPool.dealloc(p); // 把内存放回池子
当然,每次都这么做会很容易出错,一个稍好的办法是定义两个全局函数封装这两步:
BigInt* newBigIntFromPool(MemoryPool* m);
deleteBigIntFromPool(BigInt* p);
当然,每个 class 都要定义两个额外的函数,这也是个不小的负担,于是我们可以用
类模板来实现 Factory 模式。
template<typename T>
class PoolFactory : boost::noncopyable
{
public:
explicit PoolFactory(MemoryPool* pool) : pool_(pool)
{
}
T* newObject() const
{
void* p = pool_->alloc(sizeof(T));
assert(p != NULL);
return new (p) T();
}
void deleteObject(T* p) const
{
assert(p != NULL);
p->~T();
pool_->dealloc(p);
}
private:
MemoryPool* pool_;
};
typedef PoolFactory<BigInt> BigIntFactory; // 这句放到每个具体 class 的定义之后
使用的时候,如果 memoryPool 全局只有一个且线程安全的话,对象的申请和释放可以这么写:
BigInt* p = BigIntFactory(&memoryPool).newObject(); // 构造
BigIntFactory(&memoryPool).deleteObject(p); // 释放
这里构造了临时对象 BigIntFactory(&memoryPool) ,由于编译期会把函数内联展开,
并不会有任何额外开销。
必要的话还可以定义两个宏(我很不喜欢宏)来减少重复,比如
#define NEW_FROM_POOL(TYPE, POOL) TYPE##Factory(&POOL).newObject()
#define DELETE_FROM_POOL(TYPE, POOL, PTR) TYPE##Factory(&POOL).deleteObject(PTR)
用的时候
BigInt* p = NEW_FROM_POOL(BigInt, memoryPool);
DELETE_FROM_POOL(BigInt, memoryPool, p);
这个 PoolFactory 借鉴了 STL allocator 的实现。虽然我怀疑 STL allocator 的存在价值,
不过学习它的代码是没啥坏处的。
我的回复:
嗯,谢谢你。
你回的很长,很认真,我需要仔细理解一下,暂时还在看。
我们做的是运营级服务器,IP语音的,属于全球运营,因此是7*24连续工作,不允许关机的。
其实很多嵌入式设备也是这样,如果你家里有无限AP,或者用小路由器分网段,你可以想象一下,这些小家伙,是不是一直不断电的。
你的方法直觉上是可行的,因为指定内存区块去new一个对象,是可以的。
我之所以没用,有两个方面,一个是这对调用者有一定技术要求,api会比较有深度,我不敢保证我每个程序员都有这个技术实力,不会用错。嗯,还有一个就是,arm-linux-gcc,貌似还有个arm-etc-gcc(没用过,记不清了),这两种编译器,无法在小内存环境下正常编译模板,其实我书中的多线程安全的变量模板,就是这个原因被废了的。本来还是挺好用的一个东东,呵呵。因此,我有点怕在跨平台开发时,大规模使用C++的高阶特性。
书中很多奇怪的设计,其实都是因为有这样那样的顾虑存在,导致被迫这么做的。
嗯,多说一点我对宏的看法,原则上,公开场合,我一般不说宏的好话,呵呵,goto也说得少。
我之所以近几年有多用宏的趋势,是有点想向函数式开发靠拢的意思。宏我比较看重它固化计算的作用,而且,它不检查变量类型(嗯,这个有时候我觉得是优点),它的结果,取决与运行期计算结果,而不是预设值,这些其实都符合函数式编程的规范。
我也在探讨,能不能用宏来整理一套C实现函数式编程的方法,起码是具有某种函数式开发特性的方法。原因很简单,函数式编程无变量,天生具有线程安全性,仅此一条性能,已经足以让我流口水了,呵呵。
因此,很多时候,我在用宏时,脑子里想的是函数式编程。
我个人建议哈,对一个东东,既不说一定好,也不说一定不好,我喜欢走起来看,什么时候,什么地点,合用,不妨用一点,不合用呢,不用好了。
嗯,有个问题我一直想补充,结果搞忘了。呵呵。
在我的团队中,有个铁律,永远不准在非简单变量之外的其他类型变量之间发生赋值动作。
说白了,就是对象不能等于对象。
原因很简单,根据经验,目前的C++程序员,大多数对这类拷贝构造函数操作都不好,不是说他们不会写,是他们说不清楚,使用了拷贝构造函数,中间那个临时对象的生命周期是多长,以及到底位于哪块内存区域,这个临时对象本身是不是线程安全的。这往往是造成bug的根源,而且,很不好查找。
我们规定,对象传递,一定使用传址方式,不允许传值。就是想从团队规范上,规避这一大类bug。
如果code review的时候,我发现这种设计,通常是无条件打回重做。
通理,函数不允许返回对象,函数甚至不允许返回除了void,bool,int之外的其他变量类型,这也是非常严格的规避bug措施。
因此,类中拷贝构造函数,长期以来被我看做是制造bug的根源,和你不喜欢宏一样,我直觉上就反感它。因此,我的设计中,不考虑拷贝构造函数这类设计,因为我们的设计规范中,不允许有用到它的地方。这好比线程ID一样,我认为线程ID唯一的作用就是killthread,这简直是教人犯罪,因此,我的程序原则上不保存线程ID,不给大家犯错误的机会。
指针一般的原则,是上层分配好,传递给下层使用,下层严禁申请对象或内存,反传给上层使用,因为这往往是泄漏的开端。如果实在有这个设计需求,比如某个传输底层内核,收到报文是临时分配的内存,需要通知给上层,我们的原则是能传值则尽量传值,比如拷贝到上层给出的一个buffer中,实在不能传值,则使用回调,坚决遵守“谁申请,谁释放”的原则。
指针使用还有一个禁忌,是我坚持得非常好的原则。“永远不许修改指针的指向,只能修改指针指向地址的值”。
这是铁律,n多的bug,其实都是因为大家把指针改来改去,最后发生了泄漏或野指针。因此,干脆从规范上予以禁止。
这句话的推论是,永远在程序中,不允许存在两个以上的*号,即**。
实在需要修改指针的值,比如某些CreateToken功能的函数,要创建一个规定格式的结构体什么的,有两个解决方案。
你回的很长,很认真,我需要仔细理解一下,暂时还在看。
我们做的是运营级服务器,IP语音的,属于全球运营,因此是7*24连续工作,不允许关机的。
其实很多嵌入式设备也是这样,如果你家里有无限AP,或者用小路由器分网段,你可以想象一下,这些小家伙,是不是一直不断电的。
你的方法直觉上是可行的,因为指定内存区块去new一个对象,是可以的。
我之所以没用,有两个方面,一个是这对调用者有一定技术要求,api会比较有深度,我不敢保证我每个程序员都有这个技术实力,不会用错。嗯,还有一个就是,arm-linux-gcc,貌似还有个arm-etc-gcc(没用过,记不清了),这两种编译器,无法在小内存环境下正常编译模板,其实我书中的多线程安全的变量模板,就是这个原因被废了的。本来还是挺好用的一个东东,呵呵。因此,我有点怕在跨平台开发时,大规模使用C++的高阶特性。
书中很多奇怪的设计,其实都是因为有这样那样的顾虑存在,导致被迫这么做的。
嗯,多说一点我对宏的看法,原则上,公开场合,我一般不说宏的好话,呵呵,goto也说得少。
我之所以近几年有多用宏的趋势,是有点想向函数式开发靠拢的意思。宏我比较看重它固化计算的作用,而且,它不检查变量类型(嗯,这个有时候我觉得是优点),它的结果,取决与运行期计算结果,而不是预设值,这些其实都符合函数式编程的规范。
我也在探讨,能不能用宏来整理一套C实现函数式编程的方法,起码是具有某种函数式开发特性的方法。原因很简单,函数式编程无变量,天生具有线程安全性,仅此一条性能,已经足以让我流口水了,呵呵。
因此,很多时候,我在用宏时,脑子里想的是函数式编程。
我个人建议哈,对一个东东,既不说一定好,也不说一定不好,我喜欢走起来看,什么时候,什么地点,合用,不妨用一点,不合用呢,不用好了。
嗯,有个问题我一直想补充,结果搞忘了。呵呵。
在我的团队中,有个铁律,永远不准在非简单变量之外的其他类型变量之间发生赋值动作。
说白了,就是对象不能等于对象。
原因很简单,根据经验,目前的C++程序员,大多数对这类拷贝构造函数操作都不好,不是说他们不会写,是他们说不清楚,使用了拷贝构造函数,中间那个临时对象的生命周期是多长,以及到底位于哪块内存区域,这个临时对象本身是不是线程安全的。这往往是造成bug的根源,而且,很不好查找。
我们规定,对象传递,一定使用传址方式,不允许传值。就是想从团队规范上,规避这一大类bug。
如果code review的时候,我发现这种设计,通常是无条件打回重做。
通理,函数不允许返回对象,函数甚至不允许返回除了void,bool,int之外的其他变量类型,这也是非常严格的规避bug措施。
因此,类中拷贝构造函数,长期以来被我看做是制造bug的根源,和你不喜欢宏一样,我直觉上就反感它。因此,我的设计中,不考虑拷贝构造函数这类设计,因为我们的设计规范中,不允许有用到它的地方。这好比线程ID一样,我认为线程ID唯一的作用就是killthread,这简直是教人犯罪,因此,我的程序原则上不保存线程ID,不给大家犯错误的机会。
指针一般的原则,是上层分配好,传递给下层使用,下层严禁申请对象或内存,反传给上层使用,因为这往往是泄漏的开端。如果实在有这个设计需求,比如某个传输底层内核,收到报文是临时分配的内存,需要通知给上层,我们的原则是能传值则尽量传值,比如拷贝到上层给出的一个buffer中,实在不能传值,则使用回调,坚决遵守“谁申请,谁释放”的原则。
指针使用还有一个禁忌,是我坚持得非常好的原则。“永远不许修改指针的指向,只能修改指针指向地址的值”。
这是铁律,n多的bug,其实都是因为大家把指针改来改去,最后发生了泄漏或野指针。因此,干脆从规范上予以禁止。
这句话的推论是,永远在程序中,不允许存在两个以上的*号,即**。
实在需要修改指针的值,比如某些CreateToken功能的函数,要创建一个规定格式的结构体什么的,有两个解决方案。
第一,仿造微软办法,设置P变量,然后显式传址调用,防止误读。
typedef strcut _TEST_STRUCT_
{
}*PSTestStruct,STestStruct;
void Func(PSTestStruct& pStruct)
{
}
能明白吗?
typedef strcut _TEST_STRUCT_
{
}*PSTestStruct,STestStruct;
void Func(PSTestStruct& pStruct)
{
}
能明白吗?
第二,使用结构体封装
typedef strcut _MALLOC_TEST_STRUCT_PARAM_
{
STestStruct* m_pStruct;
}SMallocTestStructParam;
void Func(SMallocTestStructParam* pParam)
{
pParam->pStruct=malloc(...); //注意,这符合修改指针指向地址的值,而不是修改指针的值,这句话的指针,指SMallocTestStructParam*
}
以上是我开发服务器等高稳定性程序的一点团队经验,我想,既然你也做类似的工作,可能有用,不妨参考一下。
当然,我说得不一定全对,欢迎探讨。
typedef strcut _MALLOC_TEST_STRUCT_PARAM_
{
STestStruct* m_pStruct;
}SMallocTestStructParam;
void Func(SMallocTestStructParam* pParam)
{
pParam->pStruct=malloc(...); //注意,这符合修改指针指向地址的值,而不是修改指针的值,这句话的指针,指SMallocTestStructParam*
}
以上是我开发服务器等高稳定性程序的一点团队经验,我想,既然你也做类似的工作,可能有用,不妨参考一下。
当然,我说得不一定全对,欢迎探讨。
=======================================================
后面是一些IM即时沟通信息:
肖舸
:
其实有时候,我的库很多都在为
arm
做让步。
arm
最多开
30
条线程,我才想到,要做个任务池,进一步抽象。
实现任务片段与线程割裂,达到大吞吐量的目的
陈硕
:
我是真正把
C++
当成一门与
C
不同的,高级的语言来用。也不用为移植性考虑,没有
ARM
开发的需求。
肖舸
:
嗯,所以你当时说的我都认同,但是在我们这些草根环境下,很多情况没有你的环境优秀,只有人将就机器了
C++
对我最大的好处,其实就是对象的组织性,做大工程,真方便,我用
C++
,用的最多的就是这个
陈硕
:
任务池那一章我还没有仔细看,既然我们都有
2440
的板子,或许可以一起研究研究,做点基础工作。
肖舸
:
其实啊,这么说吧,我书里面的代码,
arm9
都跑过的
仅仅需要把线程池的最大线程数,从 300 降到 30 , arm9 就可以跑我的库
这是唯一一处移植修改
我其实一直有个想法,这个工程库可能以后最大的商业应用,其实是在嵌入式上面。
仅仅需要把线程池的最大线程数,从 300 降到 30 , arm9 就可以跑我的库
这是唯一一处移植修改
我其实一直有个想法,这个工程库可能以后最大的商业应用,其实是在嵌入式上面。
肖舸
:
我里面说了一个看法,
C++
并不是好的服务器开发语言
嗯,包括嵌入式
我不知道你能不能理解这句话
嗯,包括嵌入式
我不知道你能不能理解这句话
陈硕
:
如果你强调
7x24
的话
我没有发现更好的工业化语言,在服务器开发方面,
java
或许能满足一定的需求
肖舸
:
其实我最欣赏你的,是你反对异常
你能说说为什么反对吗?我看看和我理由一样不?
陈硕
:
我觉得在
C++
里乱抛异常就跟随处大小便一样可恶。
C++
的异常与整个语言风格格格不入,是个怪胎,异常加入
C++
的时间也很晚,比模板还晚,目前还没有形成公认的好的实践。
肖舸
:
嗯,你的是一个解释
我来说说我的解释,你看同意不
我在做程序,强调分层,业务层和功能层
层与层之间,以 api 或者 npi 松散耦合
api 在 C 和 C++ 中,通常被定义在 h 文件里面
但是,由于异常的加入,在 h 中很难精确标明一个函数的所有交互手段
即,我们即使看见了函数原型,都不足以表达该函数所有可能的异常
这意味着, api 模糊不清
后续程序员在使用库的时候,极其有可能因为某个异常没有捕捉,导致崩溃
而异常本身就是低概率时间,测试很难测试到
因此,这种崩溃可能发生在运行期,而且,由于进程被操作系统 kill
导致 debug 信息无法跟踪,该 bug 无法解决
ok ?
我来说说我的解释,你看同意不
我在做程序,强调分层,业务层和功能层
层与层之间,以 api 或者 npi 松散耦合
api 在 C 和 C++ 中,通常被定义在 h 文件里面
但是,由于异常的加入,在 h 中很难精确标明一个函数的所有交互手段
即,我们即使看见了函数原型,都不足以表达该函数所有可能的异常
这意味着, api 模糊不清
后续程序员在使用库的时候,极其有可能因为某个异常没有捕捉,导致崩溃
而异常本身就是低概率时间,测试很难测试到
因此,这种崩溃可能发生在运行期,而且,由于进程被操作系统 kill
导致 debug 信息无法跟踪,该 bug 无法解决
ok ?
陈硕
: ok,
我同意你的关点,
C++
异常较难成为
API
接口的一部分,这可以成为一篇博客。另外我补充一点,由于异常发生时栈会展开,如果因为外层没有捕捉而崩溃,那么就算有
core dump
,也不知道到底在哪儿触发的异常。这个可以解决,但
C++
默认的异常是不带调用栈信息的
肖舸
:
你补充这点很重要
另外,
npi
的问题
谁能见到一个异常可以跨机器传递?
陈硕 : 异常不但不能跨机器,也不能跨进程,还不能跨模块,要正确使用太困难了,比写对多线程程序还难,我水平有限,只好割爱不用了。
谁能见到一个异常可以跨机器传递?
陈硕 : 异常不但不能跨机器,也不能跨进程,还不能跨模块,要正确使用太困难了,比写对多线程程序还难,我水平有限,只好割爱不用了。
肖舸
:
对头
而且,异常,相当于没有标明出发点的一次 goto
而且,异常,相当于没有标明出发点的一次 goto
陈硕
:
这个很形象
肖舸
:
很难在异常处理中,精确检测当前程序的状态,自然无法恢复现场
异常,通常伴随泄漏
或者, double lock
异常,通常伴随泄漏
或者, double lock
陈硕
:
异常和泄漏这个倒是很容易解决,用析构函数封装释放操作就行,黑话叫
RAII
肖舸
:
呵呵
嗯,我觉得异常不是好东西
嗯,我觉得异常不是好东西
陈硕
:
在
C++
里不好,在
Java
里还行,不算差。
肖舸
:
我不喜欢别人打破我的程序流程,另起一个流程
我没有研究过 Java
不过, Java 既然支持多线程
一定有锁
异常,如果抛出的是对象
就是我说的,对象的拷贝构造函数概念,我很反感
因为我不知道它是否线程安全
我没有研究过 Java
不过, Java 既然支持多线程
一定有锁
异常,如果抛出的是对象
就是我说的,对象的拷贝构造函数概念,我很反感
因为我不知道它是否线程安全
陈硕
: java
有
try {} catch {} finally {}, finally {}
里边能保证解锁,这点比
C++
优越。
肖舸
:
我还真不知道
它为异常可以做个收拢的动作,嗯,不错
它为异常可以做个收拢的动作,嗯,不错
陈硕
:
对,先不管你抛啥,我把我该做的做了,再接着来
肖舸:
晕哦,如果
C++
有这个,我也许就不会这么反对异常了
嗯,我写程序,谁申请谁释放,是个规则
但仅仅是一个推论
其实根源是
谁拉出来的屎,麻烦自己擦干净
呵呵
嗯,我写程序,谁申请谁释放,是个规则
但仅仅是一个推论
其实根源是
谁拉出来的屎,麻烦自己擦干净
呵呵
陈硕
:
呵呵,话糙理不糙,多线程下的对象生命期管理是个大问题,尤其是在
C++
里。我正在写一篇博客,仔细谈谈这个事情。
肖舸
:
嗯,其实我的书试图解决的也是这个问题
肖舸
:
这么说吧
既然聊到这里了
我还是讲点我的想法
如果你看我的书
MBool , Mint ,不管你赞不赞同,我建议你看看
因为它们体现资源锁概念
其实这两个类不重要,重要的是,我的资源锁概念很实用
另外,第九章,时间片,我私人建议你重点关注
里面有很多我理解的并行开发概念
既然聊到这里了
我还是讲点我的想法
如果你看我的书
MBool , Mint ,不管你赞不赞同,我建议你看看
因为它们体现资源锁概念
其实这两个类不重要,重要的是,我的资源锁概念很实用
另外,第九章,时间片,我私人建议你重点关注
里面有很多我理解的并行开发概念
陈硕
:
嗯,好的。
ok, 等我把书从同事那里拿回来一定认真拜读
ok, 等我把书从同事那里拿回来一定认真拜读
肖舸
:
就我目前理解,资源锁是最能解决团队
bug
的
我今天还在和同事讨论这个问题,我给她的建议就是,资源锁
我今天还在和同事讨论这个问题,我给她的建议就是,资源锁
陈硕
: MBool MInt
等于把每个成员函数的起止都用锁框住了,类似
Java
里的
synchronized
方法
肖舸
:
哎,对了,麻烦你一件事
如果你在书里面,发现 bug
请务必给我信息
我好在重印时修订
如果你在书里面,发现 bug
请务必给我信息
我好在重印时修订
陈硕
: ok,
那我得认真再读一遍了
肖舸
:
笔误啊
至于技术争论,那等我们争论完了再说
至于技术争论,那等我们争论完了再说
陈硕
:
呵呵,好说
前面我说异常不带调用栈信息,这个比较好解决,在构造异常对象的时候,调用一次 backtrace() 就能记住调用栈。这个函数在 gcc 里有
前面我说异常不带调用栈信息,这个比较好解决,在构造异常对象的时候,调用一次 backtrace() 就能记住调用栈。这个函数在 gcc 里有
肖舸
: VC
有没?
陈硕 : VC 不是叫这个名字,程序也能主动查调用栈,我找找例子
陈硕 : VC 不是叫这个名字,程序也能主动查调用栈,我找找例子
VC
查调用栈的那个我找到了,有两篇文章
http://www.codeproject.com/KB/applications/leakfinder.aspx
http://www.codeproject.com/KB/applications/leakfinder.aspx
本文转自 tonyxiaohome 51CTO博客,原文链接: http://blog.51cto.com/tonyxiaohome/260742 ,如需转载请自行联系原作者