多任务开发C++风格讨论

多任务开发C++风格讨论(有新增) 2010-01-08 12:51

陈硕先生是位资深的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 泄漏,虽说办法土,倒也是挺奏效的。说实话,我挺佩服作者在缺乏工具的情况下自己设法制造工具解决问题的能力。 
 
我不后悔买了这本书。总的来说,这本书值得去读,可以以很低的代价了解别人和别的公司在工作中是怎么做的。这些做法够不够好,是不是能更好,自己遇到了如何解决?这又能引发思考,并提供了很多讨论话题。

我的回复:

  嗯,我也说几句。 
几位大牛确实一针见血,说出了我很多缺点和弱点,在此表示感谢。 
嗯,我还是申辩几句哈。 
没有完全使用C++,固然有我本人水平不够的影子,呵呵,我自我评价,我是标准的山寨C++程序员,嘿嘿。 
但,中间大牛们百思不得其解的一个奇怪写法,我这里解释一下。 
为什么不去直接用C++的类封装,而是费尽心机写了一个结构体封装类成员数据,然后写一堆C函数,然后再用类来封装。这是有原因,原因呢,书里面也写了。 
关键是为了预防内存碎片。 
C和C++的动态内存分配机制,有个问题,长期,大量申请和释放后,会有内存碎片,严重的会导致服务器挂死,而我开发的很多工程,都有7*24小时工作需求,因此,内存碎片普通程序员可能都感觉不到,却成了我必须解决的难题。 
因此我做了个内存池,主要是基于重用理念,仿造STL的内存池构建方法,建立自己的多线程安全内存池。 
但是,这个内存池有个缺点,只能提供malloc和free服务,不具备new和delete操作,我编译原理学得不好,确实不知道怎么做,呵呵。 
这就说明,我的内存池,只能为纯数据区块服务,无法应对动态对象的申请和释放。 
但是,C++的对象在大规模工程组织中的便利性我又舍不得放弃,没办法,我就做了个奇奇怪怪的方法。 
对于某些高频申请和释放,且长度不等的应用需求,比如,传输服务器常见的动态哈希表,用于目标地址,或者一些报文的快速存放和取出的工具,我习惯于这么做。 
1、类里面手工区别成员变量和成员函数,先用C的struct实现成员变量组,然后用纯C方式提供访问逻辑。 
2、用类封装,封装时,内部所有成员变量利用一个struct指针来表示,这时候,这个struct可以在内存池上分配。 
我们知道,通常一个对象的绝对尺寸,主要还是取决与内部的数据,函数表占不了多少字节的,几十个字节顶天了,当占主要尺寸的内部数据区使用内存池管理,规避了内存碎片之后,几十个字节的对象,一般说来,随便new和delete,都不会再有内存碎片的风险了。 

这实际上是根据实际使用需求,强行拆裂C++对象中数据与方法的集合,使之分别从不同的内存区申请内存,以实现尽可能的运行安全性,避免内存碎片的影响。当然,这里面还有个用以,我做工程的环境,很多时候不一定是C++环境,有可能某些时候,就碰到纯C的环境,这么开发的话,很多基本的工具,我可以同时提供纯C和C++两套api,调用更加方便。
主要目的就是这个,不知道我讲清楚了没有。 
这是实战出来的经验,因为过去很多次,我的服务器就是因为这个原因挂掉的,而且,很不好找原因。 
嗯,再说一点,原子锁我研究过,不过,我发现,原子锁和操作系统关系太密切,我要求我的库是跨平台的,嗯,这个就不劳大家批评了,这是我的设计需求。 
因此,我一般不太喜欢沾操作系统太多的东西,所以,我在锁上面又做了一层封装,实现了跨平台的安全调用机制,至少从我的工作中,这种努力时值得的,因为我通常同时写一个传输协议的客户端和服务器端,而客户端使用Windows平台,服务器端是Linux平台,二者还都有可能是arm linux平台。 
这么写,看似效率不高,很怪异,但总好过我同时维护几套库。大家说是不是? 
嗯,析构函数内无意义的加锁和解锁,我是作为一个小经验分享的,叫做有它更好,没有也无所谓。 
近年来,我使用标准成员函数定名的方式,以Start和Stop作为一个包含线程的类的显示起停标志,并且使用防止重入锁规避重入错误,已经是很成熟的编程手法。 
目前我开发时,析构函数习惯性调用Stop,确保本类所属所有线程均已经安全退出再释放,其实已经足以保证安全性了,嗯,书上的这个无意义加锁和解锁,其实在很多时候没必要。 
我提出这个经验值,主要是考虑到,有些纯工具函数类,可能被n个线程调用,如果程序员不小心,可能会在析构对象时,会导致还有线程没有退出,在访问这个对象,导致挂死。 
由于这种情况往往多发生在程序退出时,其实就是有个时间差,也许由于程序员不小心,会有一些线程多活几个周期,多碰几次这个对象,这个,我能避免,但不是所有的程序员都能避免。 
因此,我介绍这个经验,就是尽最大可能,帮助程序员在没有控制好的时候,能规避一点,就规避一点。 
我也同意,这不是解决问题正确之道,但,有时候能救命,呵呵。 
 
最后我还是要说,我仅仅是一个普通程序员,书中是我自己的一点实战经验,理论可能不一定全对,不过,几乎每个知识点,都是经过bug总结出来的,背后,往往都是十天半个月的加班,痛定思痛,总结一点,呵呵,说经验,都还不如说教训比较准确。 
我从来没有认为自己有能力代表国内程序员的水平,我甚至连我们公司都代表不了,我仅仅代表我自己。 
 
最后,还是谢谢各位大牛的关注,如果有问题,欢迎继续讨论。
 

补充一点,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功能的函数,要创建一个规定格式的结构体什么的,有两个解决方案。
第一,仿造微软办法,设置P变量,然后显式传址调用,防止误读。
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*
}

以上是我开发服务器等高稳定性程序的一点团队经验,我想,既然你也做类似的工作,可能有用,不妨参考一下。
 
当然,我说得不一定全对,欢迎探讨。
 

 

=======================================================

后面是一些IM即时沟通信息: 


肖舸因为我以前从6502汇编开始,做工控,做底层很多。

  其实有时候,我的库很多都在为arm做让步。

  arm最多开30条线程,我才想到,要做个任务池,进一步抽象。

  实现任务片段与线程割裂,达到大吞吐量的目的 


陈硕我是真正把C++当成一门与C不同的,高级的语言来用。也不用为移植性考虑,没有ARM开发的需求。 


肖舸嗯,所以你当时说的我都认同,但是在我们这些草根环境下,很多情况没有你的环境优秀,只有人将就机器了 

C++对我最大的好处,其实就是对象的组织性,做大工程,真方便,我用C++,用的最多的就是这个 


陈硕任务池那一章我还没有仔细看,既然我们都有 2440 的板子,或许可以一起研究研究,做点基础工作。 


肖舸其实啊,这么说吧,我书里面的代码,arm9都跑过的 
  
仅仅需要把线程池的最大线程数,从300降到30arm9就可以跑我的库 
  
这是唯一一处移植修改 
  
我其实一直有个想法,这个工程库可能以后最大的商业应用,其实是在嵌入式上面。

  arm平台,很多厂商为了降低成本 
  
情愿用arm7的板子,都很多 
  
嵌入式系统里面游戏规则不一样 
  
old的东东,越卖得好 
  
这和工控界是一样的 
  
上个月我还看见一块Z80的板子 


陈硕嗯,理解,只要芯片厂商不断货,旧的比新的成熟 


肖舸: arm7还没试过,哪天买块来玩玩,这个是应用大领域 

陈硕: 440BX,经典的 ARM7 板子 

 比起ARM,我更熟悉 51 ,毕竟在学校我还用 51 做过不少东西,

  ARM 纯粹就是买来玩的,没有项目压力。 

肖舸: C51我玩过,仅仅做过一个表达式解析器 


陈硕: 51 的能力太弱了,只能做小玩意儿,arm7 就像个大单片机,arm9像个小电脑 


肖舸: arm9我把它看做PII

  没错,arm9能做大事情的

  我的内存池哈,在arm9底下,每秒能分配10~15万次,当时吓我一跳

  我的赛扬笔记本,1.4G,每秒也才40~50万次 


陈硕嗯,差不多,98年的 PII 266/300, 64M 内存,经典配置 


肖舸没错,我的arm9400Mhz 
  
嗯,当时是这样,我买的2410 
  
回去做个百万次的加密算法,AES测试,13 
  
我嫌慢了 
  
加了80,换了2440 
  结果,6 
  
真差一半 


陈硕: 2410 慢一半,200 MHz 
  嗯,符合我的猜测 :) 

肖舸 
  
这还真是实测 
  
我喜欢做这种实测 
  
其实,很多库我都测过,结果被我废了 


陈硕嗯,挺好,实践出真知,能掌握第一手资料 


肖舸你有没有试过,uclinux下的gcc,是不支持模板的?


陈硕我还没有试过,想来没道理不支持,至少版本也得大于 2.95.3  
  
我有空试试


肖舸不清楚,2007年,我的多线程安全的变量模板,就是这么被上海的兄弟们废了 
  
当时很郁闷,可惜,我一直没有测试过 


陈硕国内很多嵌入式教程选用的软件版本很低,而且没有升级的意思 

肖舸这你倒是误会了 
  
实际工作中,刚才说了,版本越低,工作机会越多。。。 
  
上周还有个娃娃问我 
  
我建议他们用arm9 
  
最后他们选型arm7 
  arm9
80一片 
  arm7
30 
  
量产价哈 


陈硕哦,我觉得 gcc 反正是交叉编译,又不是在 uclinux 上运行,编译都是在主机上进行,然后把文件拷过去,模板不模板的对于可执行文件又没区别。 

肖舸不是这个道理 
  
交叉编译器,也要考虑目标运行平台

  我里面说了一个看法,C++并不是好的服务器开发语言 
  
嗯,包括嵌入式 
  
我不知道你能不能理解这句话 

陈硕如果你强调7x24的话 

肖舸对头 


陈硕我没有发现更好的工业化语言,在服务器开发方面,java 或许能满足一定的需求 


肖舸嗯,还有个原因 
  C++
对内存的使用,你不觉得太奢侈了吗?


陈硕呃,说实话不觉得,每个对象的大小是可知的,new 一个 class  malloc 一个 struct 消耗的内存一样多。 
  
如果你指编译后的代码大小,那我部分同意 


肖舸关键问题出在你我都讨厌的一个东东 
  
继承 
  
不是代码大小,而是其运行空间要求,比较奢华 
  C++
的开发者,没有考虑很多极限情况

  比如,64Marm 


陈硕继承在没有虚函数的情况,没有额外的内存开销 
  其实C++的诞生环境比这个还恶劣,8283年那会儿4M都是很奢侈的 


肖舸可是,谁玩继承会不玩虚函数? 
  
其实,模板也很奢侈,起码我这么认为 


陈硕虚函数的话,每个对象大4个字节,用了存虚表指针,似乎也不算很多 

肖舸不是,嗯,这个也是我的感觉,也没做太多测试 
  
我总觉得,泛型设计,考虑问题太多,内存耗用过大 


肖舸那会没有STL。。。 

陈硕: STL9394年,那会儿他们可能已经有 64M 内存的工作站了。 


肖舸我是appleII走过来的,对内存很敏感 
  
当时,Dos3.3 
  
里面有三段空白地址,大约192字节 
  
我做了个病毒 

陈硕哇,厉害 

肖舸所以,我这一代程序员,对内存的斤斤计较,确实比很多人都敏感 


陈硕模板在嵌入式方面的应用目前还没有成功的例子可以仿照,所以大家谁都不愿意尝鲜,这可以理解 

肖舸

  其实我最欣赏你的,是你反对异常 
  
呵呵 

陈硕呵呵 


肖舸你能说说为什么反对吗?我看看和我理由一样不? 

陈硕我觉得在 C++ 里乱抛异常就跟随处大小便一样可恶。C++ 的异常与整个语言风格格格不入,是个怪胎,异常加入 C++ 的时间也很晚,比模板还晚,目前还没有形成公认的好的实践。 


肖舸嗯,你的是一个解释 
  
我来说说我的解释,你看同意不 
  
我在做程序,强调分层,业务层和功能层 
  
层与层之间,以api或者npi松散耦合 
  api
CC++中,通常被定义在h文件里面 
  
但是,由于异常的加入,在h中很难精确标明一个函数的所有交互手段 
  
即,我们即使看见了函数原型,都不足以表达该函数所有可能的异常 
  
这意味着,api模糊不清 
  
后续程序员在使用库的时候,极其有可能因为某个异常没有捕捉,导致崩溃 
  
而异常本身就是低概率时间,测试很难测试到 
  
因此,这种崩溃可能发生在运行期,而且,由于进程被操作系统kill 
  
导致debug信息无法跟踪,该bug无法解决 
  ok ?

陈硕: ok, 我同意你的关点,C++异常较难成为API接口的一部分,这可以成为一篇博客。另外我补充一点,由于异常发生时栈会展开,如果因为外层没有捕捉而崩溃,那么就算有core dump,也不知道到底在哪儿触发的异常。这个可以解决,但 C++ 默认的异常是不带调用栈信息的 


肖舸: yeah 
  
你补充这点很重要

  另外,npi的问题 
  
谁能见到一个异常可以跨机器传递? 
 
陈硕异常不但不能跨机器,也不能跨进程,还不能跨模块,要正确使用太困难了,比写对多线程程序还难,我水平有限,只好割爱不用了。 

肖舸对头 
  
而且,异常,相当于没有标明出发点的一次goto 

陈硕这个很形象 


肖舸很难在异常处理中,精确检测当前程序的状态,自然无法恢复现场 
  
异常,通常伴随泄漏 
  
或者,double lock 


陈硕异常和泄漏这个倒是很容易解决,用析构函数封装释放操作就行,黑话叫RAII 


肖舸呵呵 
 嗯,我觉得异常不是好东西 

陈硕C++里不好,在Java里还行,不算差。 

肖舸我不喜欢别人打破我的程序流程,另起一个流程 
  我没有研究过Java 
  
不过,Java既然支持多线程 
  
一定有锁 
  
异常,如果抛出的是对象 
  
就是我说的,对象的拷贝构造函数概念,我很反感 
  
因为我不知道它是否线程安全 

陈硕: java  try {} catch {} finally {}, finally {} 里边能保证解锁,这点比 C++ 优越。 

肖舸我还真不知道 
  
它为异常可以做个收拢的动作,嗯,不错 

陈硕对,先不管你抛啥,我把我该做的做了,再接着来 

肖舸晕哦,如果C++有这个,我也许就不会这么反对异常了 
  
嗯,我写程序,谁申请谁释放,是个规则 
  
但仅仅是一个推论 
  
其实根源是 
  
谁拉出来的屎,麻烦自己擦干净 
  
呵呵 

陈硕呵呵,话糙理不糙,多线程下的对象生命期管理是个大问题,尤其是在C++里。我正在写一篇博客,仔细谈谈这个事情。 

肖舸嗯,其实我的书试图解决的也是这个问题 


肖舸这么说吧 
  
既然聊到这里了 
  
我还是讲点我的想法 
  
如果你看我的书 
  MBool
Mint,不管你赞不赞同,我建议你看看 
  
因为它们体现资源锁概念 
  
其实这两个类不重要,重要的是,我的资源锁概念很实用 
  
另外,第九章,时间片,我私人建议你重点关注 
  
里面有很多我理解的并行开发概念 

陈硕嗯,好的。 
  ok, 
等我把书从同事那里拿回来一定认真拜读 


肖舸就我目前理解,资源锁是最能解决团队bug 
  
我今天还在和同事讨论这个问题,我给她的建议就是,资源锁 

陈硕: MBool MInt 等于把每个成员函数的起止都用锁框住了,类似Java里的synchronized 方法 


肖舸哎,对了,麻烦你一件事 
  
如果你在书里面,发现bug 
  
请务必给我信息 
  
我好在重印时修订 

陈硕: ok, 那我得认真再读一遍了 

肖舸笔误啊 
  
至于技术争论,那等我们争论完了再说 

陈硕呵呵,好说 
 前面我说异常不带调用栈信息,这个比较好解决,在构造异常对象的时候,调用一次 backtrace() 就能记住调用栈。这个函数在 gcc 里有 

肖舸: VC有没? 
陈硕: VC 不是叫这个名字,程序也能主动查调用栈,我找找例子

  VC 查调用栈的那个我找到了,有两篇文章  
  http://www.codeproject.com/KB/applications/leakfinder.aspx

  http://www.codeproject.com/KB/threads/StackWalker.aspx?fid=202364&fr=26 

 

=======================================================

我的新书《0 bug ---- C/C++商用工程之道》已经由电子工业出版社通信分社出版,如果大家希望网购,建议去当当购买 ,据朋友们反应,这家网店信誉度好,发货快捷,而且打折很多,比较实惠。
当当:
http://product.dangdang.com/product.aspx?product_id=20738772

如果大家看了,觉得我的书好,有帮助,麻烦去写点书评,我需要大家的鼓励,也希望这本书能帮助更多的人。
如果大家看了,觉得不好,是垃圾,我也为大家准备了一个出气发飙的地方,就在我的读者俱乐部(书中有网址),大家有什么不满,请尽管发泄,我会一直改这本书,改到大家满意为止。

读者QQ群(都是读者自建,我作为作者加入,由于QQ群的信息不好长期保留,大家有问题讨论,还请去读者俱乐部)

1#:43309956    2#:33076658    3#:93769735

肖舸

如果您对我的文章感兴趣,请点这里添加我为好友:http://student.csdn.net/invite.php?u=39028&c=42fcd4a519102d74
如果您喜欢我的文章,请点这里投我一票:
http://student.csdn.net/huodong.php?do=toupiao&pid=1&uid=39028
博客主站:
http://blog.csdn.net/tonyxiaohome
CSDN学生大本营个人主页:
http://student.csdn.net/?39028

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值