这本书或许能从一个侧面反映国内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+ 上有效)。
举这个例子,是想说明编写线程安全的代码(遑论实现线程同步原语)是件多么困难的事情。开发者需要深入理解多处理器的内存模型、乱序、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++》上反复强调的内容。(另一方面,这说明 C++ 语言在缺省状态下是不安全的,我认为 class 的拷贝构造和赋值应该默认是 private 的才合理。struct 可以保持 public。)
作者在第 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+ 上有效)。
举这个例子,是想说明编写线程安全的代码(遑论实现线程同步原语)是件多么困难的事情。开发者需要深入理解多处理器的内存模型、乱序、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++》上反复强调的内容。(另一方面,这说明 C++ 语言在缺省状态下是不安全的,我认为 class 的拷贝构造和赋值应该默认是 private 的才合理。struct 可以保持 public。)
作者在第 6.1.4.6 节提到在析构函数里额外做一次加锁和解锁,防止程序在多线程下崩溃,这更是错误的,因为对象的生与死不能由该对象自身拥有的互斥器来保护。这个问题很深,但解决起来并不费劲。2009 年 12 月的上海C++技术大会上有一场《当析构函数遇到多线程》的主题演讲,将来有空我会把演讲稿整理成文放到博客上。
这本书或许能从一个侧面反映国内 C++ 开发的大体水平。C++ 照作者这么用,固然不符合我的审美和我们团队的性能要求,但也不妨碍做出质量性能合格、能卖出去的软件。特别是最后几章谈到抓内存泄漏和 Sockets 泄漏,虽说办法土,倒也是挺奏效的。说实话,我挺佩服作者在缺乏工具的情况下自己设法制造工具解决问题的能力。
我不后悔买了这本书。总的来说,这本书值得去读,可以以很低的代价了解别人和别的公司在工作中是怎么做的。这些做法够不够好,是不是能更好,自己遇到了如何解决?这又能引发思考,并提供了很多讨论话题。
有关键情节透露