半冷半暖秋天

2010-12-12

[zz]从”吵架”贴中加深对C++的理解

Filed under: C/C++ — 标签:, — sunu @ 15:20:49

无意中看到这篇文章:http://www.cppblog.com/converse/archive/2010/07/06/119427.html
以下文字,出自上文评论中名maxime的牛人,此处仅做为学习之用,未经牛人允许,勿怪。

 C++是一门在工业实践中成长起来的语言,工业界发明这些东西是因为需要,学院派却总跟不上进度,教材几十年一变。要用C++,就要做好准备,否则,你干嘛不用Java或者C#。
1. 关于所谓“频繁的构造/析构开销大”
 你首先要清楚“构造”和“析构”中编译器到底为你做了什么。
 1.)分配对象空间:如果是在堆中分配对象,那么会有一个代价很大的堆分配(new,在2.7G的CPU上单线程new性能是5M次/秒);如果在堆栈上分配,内存分配代价几乎为零。
 2)调用构造函数和析构函数.这有两个开销,一个是调用本身的开销,一个是函数体内部代码的开销,很明显,前者才C++带来的额外开销。我可以告诉你的是,如果是内联,这个开销为0,如果不是内联,这个开销在2.7G的CPU上单线程性能是1200M次/秒,作为类比,2.7G的CPU上单线程可以做400M次32位整型变量写入操作,也就是这个开销比写一个整型变量还小。
 现在,看看你说的情况,局部对象的构造和析构,每次的代价比写一个32位整型的变量还小得多,相比每次日志输出至少十几个字节的内存拷贝,这点开销完全可以忽略不计,除非打算每秒中打算做1M次的日志,它带来的代价不占用1%的CPU而已,不过事实是,每秒钟写不了1M次的文件IO。
 最后从设计的角度考虑这个问题,你的系统打算每秒中写多少次日志,应该心理有数吧,从这个意义上,从设计的角度,上面我写的那些分析毫无必要,只是为了加深对C++的理解,事实是,即便“频繁的构造/析构开销大”很大,它们仍然不是系统的真正瓶颈,没必要过早优化。如果它们真成了瓶颈,你应该做的事情是,调整成合理的日志策略。

2.所谓“比如log << "hello " << "world",是无法判断到底在输出"hello"还是"world"的时候上面的参数输入已经结束了”
 其实,这个问题,流的设计者早已考虑到了,std::endl就是用来干这件事情的。事实上,自定义的流操控符,还可以干很多事情比如:

std::cout << v1 << mylock(v2) << v2 << myunlock(v2); 

 上面的mylock,myunlock就是自定义的操作符,用来给v2加锁解锁,而不输出任何字符。它到底能做什么,取决于你的想象力。我总爱把C++比作机械行业的钳工,他们比不上机器的速度,但没他们不行,很多事情机器做不了。使用正确的工具做正确的事情,如果你感觉不对,先想想选对工具没,而不是抱怨工具很烂。
 额外,说明一点,有人告诉你sprintf存在写错的可能性,所以,你可以说,如果别人忘了写上他的endl怎么办?
 我来告诉你吧,写错了其实没什么大不了的,问题关键是,写错了会带来什么危害。sprintf写错了,可能带来的是内存溢出覆盖,这才是我们恐惧他的原因,一个内存溢出带来的危害我就不说了。 反之,少写了一个endl,最多就是两行日志重叠,或者一个日志输出时间晚了一会儿。如果你真看到这个情况,把endl加上去就行了。
 不知道现在是否能理解了,不要害怕bug,不要害怕写错,要怕会让你掉进深渊的bug。我得承认,这是C/C++的弱点,java/C#相对好很多。
 C++最害怕的,就是指针操作,内存覆盖可以毁掉整个程序的运行基础,却不容易找到错误的代码。但这也是C++的优点,C++为什么要用流替换C的sprintf,就是要减少内存覆盖错误的机会。当然,C++中仍然有这种错误的机会,因为抛弃了指针,C++和Java就没区别了。如果说C是做操作系统的,java是做应用的,C++就是做系统和应用结合部的,只有理解了这点,你才能用好C++,而不是抱怨,它既没C简单,也没java安全。
 事实是,C++就是这么个怪胎,比Java更快,比C更安全更有开发效率。

3. 关于“要使用这门语言写出正确的程序来,需要了解底下多少的细节呢?!”
 首先答案是,不需要知道细节,只需要知道“规范”。C++真正的问题不是太复杂,而是在实践中缺乏规范,尤其在中国的软件作坊里面。就像你会开汽车一样的,你没比要知道汽车发动机原理,同样能把汽车开好。因为你遵守了开汽车的规范,比如启动的时候,慢加油门。
 很多人的问题在于,在思想上,忽视了规范,到头来却怪东西太复杂。
 其次是了解细节,可以工作更深入。再说了,就算复杂,C++能有多复杂,一个C++语言里面能有多少东西呢?相比一个Java库,这点东西真算不了什么。很多人掌握不好,是因为没有正正经经的机会去学,去练。这点像数学,学的时候比较枯燥,不管怎么说,这点东西就叫复杂,那只能说,做的应用系统太简单。

4. 关于“假如需要考虑多线程的话,那么一次输入有多个函数函数中被调用”
 要在多线程进行IO操作,肯定是要用锁的,就算你不直接用,系统API的流API,比如Win32的WriteFile,也是要用的。
 所以,答案很简单,用锁。问题不在于有几次函数调用,而在于能否让这几次函数调用位于同一个锁当中。
 传统上,一个sprinf,你可以加一次锁,就够了。 而现在呢,分成了好几次调用,那么就在这几次调用之间和之后加锁就行了,在本例中,也就是那个被认为过于调用繁琐的临时对象了,在它的构造函数加锁,在它的析构函数中解锁,就能保证输出的原子性。如果这样还不满意,还可以考虑流操控符加锁,不过有点危险。
 不过呢,说道最后,如果你明白,那个看似效率低下的临时对象其实对整行的输出做了缓存,所以在glog中,临时对象中是没必要用锁的,因为临时对象中保存的字串是不会被多线程打断的,它能够保证所有的“<<”调用在输出上的原子性。最后析构函数中,真正进行输出时,在下层的实际输出位置,实际上是有锁。

5. 最后谈一下,C++流的真正缺点?
 从安全性的角度讲,C++流相对sprintf是一次飞跃。从实际项目来看,C++程序员的代码产出和维护量,通常会数倍甚至几十倍于C程序员,这表面了在某些问题域上,C++比更有开发效率。
 但由此带来的问题是,在代码量少的时候,C程序员可以花时间慢慢检查代码,保证sprintf没问题。而C++程序员再这样做效率就太低了。所以才会有了C++流的方案,C++流设计者正是从实践中品尝到了sprintf的苦果。
 事实是,C++语法形式,从实用性角度,的确很蹩脚。而且性能只有sprintf的1/3.不过实际环境下,性能通常不是问题,流输出很少会是一个应用系统真正的瓶颈。
 蹩脚的语法,是个问题,尤其当你需要做格式控制的时候,代码可能非常长。这个问题,我的看法是,写的时候可能多花点时间,不过以后维护起来就轻松了。毕竟,我宁愿选择安全性,花三天时间去找一个缓冲区溢出是不会宁人愉悦的。当你认为语法问题很重要时,通常暗示代码管理上有问题。我通常认为代码的书写只占20%的时间,80%时间是在维护代码。维护效率远比书写效率重要。
 在C++领域,新发明似乎是没有止境的,有一个新的,利用重载“()”操作符的格式化库出现了,具体我本人没有用过,看起来还不错,据说在性能上优于sprintf,在安全性上不输于C++流,在格式上类似sprintf。由于缺乏大规模应用,实际情况如何,还不好说。
 就我本人而言,我认为C++流的效率和格式问题,并非致命问题,所以也就不急着使用更先进的东西了,短期内我C++流仍是最好的格式化输出工具。除非,项目主要业务逻辑就是格式化字符串,那也许我会选择sprintf或者其他的东西。
 最后,我感觉楼主,似乎想在一个输出语句中,输出很长很长的,可能跨越多次物理输出的内容。
 这样做,首先代码不易理解,不易修改维护。 根据本人的实际经验来看,日志输出最好还是按实际物理行为单位比较好,所以glog没有支持所谓endl特性。
 楼主可能真正担心的是另一个问题,在多线程程环境下,想要连续输出的几行文本,会被其他线程打断,以致阅读性变差。对此,我建议,如果不希望被打断,使用glog那就需要八几行输出写在一个glog句子,作为一次原子输出就行了。但是,如果楼主对这样的原子输出,还要求再被分成多次物理输出,那这是为什么呢?有这个必要吗?既然打算连续输出几行,且在一个语句之中,整个语句时间是非常快的,对观察者而言,一次原子输出是由一次物理输出还是多次物理输出构成,没有任何实际意义。

2010-12-10

spserver代码阅读笔记

Filed under: C/C++,文档及资源 — 标签:, — sunu @ 12:04:24

从这里可以下载doc/pdf版:spserver代码阅读.pdf

http://code.google.com/p/spserver/

引用:http://iunknown.javaeye.com/blog/59804
spserver 是一个实现了半同步/半异步(Half-Sync/Half-Async)和领导者/追随者(Leader/Follower) 模式的服务器框架,能够简化 TCP server 的开发工作。
spserver 使用 c++ 实现,目前实现了以下功能:

  • 封装了 TCP server 中接受连接的功能
  • 使用非阻塞型I/O和事件驱动模型,由主线程负责处理所有 TCP 连接上的数据读取和发送,因此连接数不受线程数的限制
  • 主线程读取到的数据放入队列,由一个线程池处理实际的业务
  • 一个 http 服务器框架,即嵌入式 web 服务器(请参考: SPWebServer:一个基于 SPServer 的 web 服务器框架)

目录

  1. Leader/Follower 模型
  2. Dispatcher模型
  3. Hahs半同步半异步模型
  4. 代码编写

一、Leader/Follower 模型

一切从类SP_LFServer的构造函数开始。如果SP_LFServer是在主线程中实例化的,那就从主线程讲起。

以[M]代表主线程[MainThread],
[M]建立的线程池中的线程[ChildThread][C]

  1. [M]SP_IOUtils::tcpListen初始化监听socket
    • [M]创建socket listenFD
    • [M]bind listenFD到本地某端口
    • [M]listen在listenFD上
  2. [M]初始化libevent事件基
  3. [M]把listenFD的读事件绑在事件基上,回调函数是SP_EventCallback::onAccept
  4. [M]建立线程池,每个线程都跑去执行SP_LFServer ::lfHandler [C]阻塞在这里
    • [C]一旦有线连接进来,某个[C]在SP_LFServer ::handleOneEvent抢到全局锁,跑去执行一次event_base_loop,也就是执行了SP_EventCallback::onAccept把新连接接进门来。
      • [C]SP_EventCallback::onAccept中,主要是把当前连接放入会话管理器,并为新连接创建SP_Session对象,然后setHandler及setIOChannel,注册连接的读写到SP_LFServer的事件基,并宣告”此连接已经可以读写,某个[C]来处理读写事件吧”。
      • 再就是调用事务的初始化函数(由具体的Handler实现):doStart
    • [C]或者是有数据要收发,那些阻塞在SP_LFServer :: lfHandler -> SP_LFServer :: handleOneEvent中event_base_loop的[C],会被激活并调用相应的读写回调,接收数据或者向连接写数据。
    • [C]或者是有任务要处理,那些阻塞在SP_LFServer :: lfHandler -> SP_LFServer :: handleOneEvent中InputResultQueue->pop的[C],会被激活并取得这个任务。
  5. [M]做完这些事情,主线程自己就睡觉去了(以后再也不干事了) [M]阻塞在这里

此种模式中,实际的数据收发,是由某个最早抢到全局锁mMutex的[C]来做的,它不仅把新连接通过SP_EventCallback::onAccept接进来,也同时在event_base_loop中把其它事件(如果有)也一同处理了,直到某个事件向任务队列InputResultQueue塞了任务(这任务通常是接收了足够多的数据或者其它事件导致的)。

一旦这个[C]接到任务,它就跑去干正经活去,数据收发的事情交由下一个苦力接手,这就要看线程池中哪个线程抢到了,应该是不确定的。干完正经活的线程回到线程池里,跟其它人抢mMutex。

反正几乎每个[C]都会经历:抢mMutext,处理libevent事件(收发数据当然在这里)或者接到任务。

二、Dispatcher模型

以[M]代表主线程[MainThread],以[D]代表[M]创建的[DispatcherThread]线程

  1. [M]SP_IOUtils::tcpListen初始化监听socket
    • [M]创建socket listenFD
    • [M]bind listenFD到本地某端口
    • [M]listen在listenFD上
  2. [M]SP_Dispatcher:: SP_Dispatcher 初始化
    • [M]创建libevent事件基
    • [M]创建阻塞队列,存放连接
    • [M]创建新线程[DispatcherThread]
    • [D]创建两个线程池
      • [D]workerExecutor池中有较多线程,用于执行实际事务
      • [D]actExecutor池中线程较小,用于处理收尾事件
    • [D]进入event_base_loop (DispatcherThread阻塞在这里)
      • [D]有新事件发生,workerExecutor执行,actExecutor收尾
  3. [M]在循环中accept ([M]阻塞在这里)
    • 有新连接进来,激活了[M],空闲,推入阻塞连接队列
      • SP_IOUtils::setNonblock设置连接为非阻塞型
      • 触发onPush事件,激活了[D]
      • [D]为连接设置事务处理对象setHandler
      • [D]为连接初始化数据渠道(ssl/raw/ encrypted)setIOChannel
      • [D]将连接之读写事件加入Dispatcher的事件基中进行监听
        • SP_EventCallback::onRead处理读数据
        • SP_EventCallback::onWrite处理写数据
      • [D]开始调用事务处理的doStart或仅激发可读写可写消息
    • 系统忙,超过连接数,往连接里写” System busy, try again later”,断开连接

所有进来的连接,都是先经[M]Accept的,因为Accept后直接抛入连接队列,所以Accept可以很快进行,不管当前有多少连接都会非常及时响应。

待[D]开始监听新连接的读写事件后,数据的收发都是由[D]自已负责,这是在2.e步骤行的。

一旦读写完(或者处理完其它信号或者事件),就去看看任务队列InputResultQueue中有无需要处理的事件(比如在读写阶段,读完一段数据如果符合条件,就会被组成一个任务扔到任务任务队列中)。当然,[D]不会事必躬亲,而是把任务取出来扔给workerExecutor线程池。

然后,[D]会去看任务执行结果队列OutputResultQueue,这里的元素是由workerExecutor中的某线程在执行事务的时候产生的,即执行完一个任务就把任务打包放到结果队列。[D]从结果队列取出来,扔给actExecutor线程池,由它去收尾。其实一般情况下只是做些释放内存之类的工作,或者统计一下成功执行了多少任务(请求)。

三、Hahs半同步半异步模型

以[M]代表主线程[MainThread]

一切从类SP_Server::start讲起

  1. [M]SP_IOUtils::tcpListen初始化监听socket
    • [M]创建socket listenFD
    • [M]bind listenFD到本地某端口
    • [M]listen在listenFD上
  2. [M]通过SP_EventArg建立libevent事件基
  3. [M]把listenFD的读事件绑在事件基上,回调函数是SP_EventCallback::onAccept
  4. [M]建立大线程池workerExecutor和小线程池actExecutor,这些池中的线程都是阻塞在判断队列中是否有元素的函数上
  5. [M]进入libevent事件处理循环:event_base_loop [M]阻塞在这里
    • 直到有事件发生,比如新连接进来,[M]调用onAccept去迎接新连接,并将其读写事件注册到事件基上
    • 处理完事件后,去任务队列中看看有无要处理的任务(InputResultQueue),有的话提出来扔给大线程池workerExecutor,由池中的线程去干实际的活
    • 然后看看有无处理完的任务(OutputResultQueue),有的话提出来扔给小线程池actExecutor

这个模型里,所有数据读写及其它libevent事件处理都由实例化SP_Serverr 的线程来做的,如果是文件传送这样的应用,此线程会很忙。

我猜想,名字中的同步是否是指实际的事务处理是同步的,比如流程一直都是接收数据,处理数据(比如处理的时候可以很长时间),返回数据这么个固定的顺序;而实际数据的收发,在事务处理时不需要关心,只要认为数据已经发出去就好了,至于真正是在什么时候发给客户端的,事务处理函数管不着,这种不等待数据真正地外发做法是否就是异步呢?

四、代码编写

每个连接开始都是可读又可写的,根据要实现的功能的不同,可以由Client/Server任一方先出招。所有的交流总是会有某方主动的,像http服务,总是会由客户端先行发个请求,然后服务器回应。

在代码编写的时候,一般只需要实现这么几个地方:

  1. SP_MsgDecoder子类的decode
  2. SP_Handler子类的handle/start/error/timeout

SP_MsgDecoder::decode在每次接收完数据的时候被调用,返回值代表了是不是已经收到一个完整的数据包了,如果是则必须返回eOK让接受数据的函数组个任务放到任务队列,让线程池中的某位[C]来调用SP_Handler::
handle。如果还只是不完整的数据包,那就返回eMoreData,告诉接收函数我还没吃够,继续喂我吧。

这其实会有个小问题,如果我们的逻辑数据包太大,内存会占用较多,效率不高。并且这样的处理方式在需要连接接收大量数据的地方不太实用,比如像接收客户端传送的大文件。有人会说,像传文件的时候,可以设置让decode一直返回eOK嘛,这确实是可行的,但是decode函数还是会被调用N次,这个调用还是挺耗资源的。每接收一小片数据,都会去调用一下decode+handle,还有个深层次的问题是这里的decode+handle都是父类虚函数的实现,效率就更低了。

每个东西都会有它适合的应用场合,像以上的模式更适应像http或者telnet这种交互性较多,但每次传递数据较少的应用。如果是做ftp文件传送之类,还是考虑其它模型吧。

不过我倒还真是用它来实现过一个文件服务器,文件内容传送是被分片打包进自定义的数据包的,和其它命令混合着传送到服务器,服务端一片片地将文件内容写入目标文件。由于文件碎片数据包中包含了足够的信息,所以这样断断续续地写也是可以传文件的。传输速度由于只是测试过单个客户端连接的情况,所以也就没什么参考意义。

2010-12-04

DB2的sqC文件名前八位不能重复的解决办法

Filed under: C/C++,数据库 — 标签:, , , — sunu @ 16:40:26

我们的项目中使用到了IBM的DB2数据库,系统开发语言是C++,操作数据库是直接在C++中编写ESQL(嵌入式SQL)语句。在使用中发现,只要有文件名前8位相同,就造成部分文件中的代码失效。

原因:
——————
db2默认情况下使用*.sqC的名字生成*.bnd,又以*.bnd的文件名部分在数据库中做绑定,建一个同名的package。而db2的package的名字是有八字节长度限制的,造成前八位文件名相同的C++代码,总会后面的覆盖前面的。

知道原因就好办事了。

解决:
——————
取原始文件名的md5摘要前N位,拼成临时sqC文件的名字,再以此文件去预编译,继而用同名bnd文件到目标数据库做绑定,这样package就不会前后覆盖了。

makefile中实际的做法:
————————

%.C : %.sqC
  db2 connect to $(dbName) user $(dbUser) using $(dbPassword);\
  cp $*.sqC P${shell echo $* |md5sum -|cut -c1-8}_$*.sqC;\
  db2 P${shell echo $* |md5sum -|cut -c1-8}_$*.sqC bindfile;\
  db2 bind P${shell echo $* |md5sum -|cut -c1-8}_$*.bnd;\
  db2 connect reset; \
  db2 terminate;

2010.12.08 更新

  • 要改名的不是bnd文件,而是sqC,所以要在bindfile的时候已经改好名了。
  • md5散列可能出现前字母是数字,所以文件名加固定前缀。

2010-12-02

Toad for IBM DB2 FREEWARE

Filed under: 文档及资源 — 标签:, , — sunu @ 18:05:07

最近要接触Oracle/DB2数据库,试了几个图形界面客户端,这个算是最好用的。

下载地址: 免费版也超级强大

2010-11-23

Google Javascript脚本引擎v8 学习笔记

Filed under: C/C++ — 标签:, , — sunu @ 00:29:40

本来11月初就要写的日志,断断续续居然写了近半个月,晚上没心情加班,终于狠狠心把它写完了。

1. google V8学习笔记.doc
2. google V8学习笔记.pdf
3. google V8学习笔记 – 源码

目录
-------------------------
一、Google v8 整体印象
 1.1 Google v8是什么,能做什么?
 1.2 Google v8的获取及编译
  1.2.1 获取源码
  1.2.2 编译类库及示例

二、Google v8入门
 2.1 v8的基本概念
 2.2 从HelloWorld开始
 2.3 在C++中访问Javascript的变量及函数 
 2.4 在Javascript中访问C++全局函数及变量
 2.5 在C++中“声明”“Javascript类”, 供Javascript实例化
 2.6 封装完整的C++类到Javascript中,供Javascript实例化

三、Google v8的开源应用
 3.1 Node.js
 3.2 v8cgi

四、参考资料
五、本文源码
 

仅仅是学习笔记,不是教程。

Powered by WordPress