解耦

事情起源于昨天的一次讨论。模块A如何在不同的时期返回不一样的数据类型的值,供其他不同的模块使用。

我自行脑补了一下,其实这类问题可以归结为对数据类型的解耦。

考虑一种特殊情况,有moduleA,moduleB,moduleC1,moduleC2等四个模块。moduleA在不同的时期产出不同的数据供moduleC1和moduleC2处理。moduleB用于将moduleA产出的数据根据具体情况情况分别调用moduleCx来处理。moduleCx的接口类似moduleC1::doSomething(type1 data);moduleC2::doSomething(type2 data); 

最low的解法莫过于在moduleB中分别定义type1,type2的变量接收一下,然后根据具体情况分别传入相应的moduleCx模块中。然而这样的写法根本不符合open-close原则。moduleCx从属性上来讲应该属于同一种东西的不同变种,以后会有很大的可能增加moduleC3(type3);moduleC4(type4)等模块。几乎每增加一种模块,moduleB都需要相应作出修改。

这样,moduleCx已经与moduleB深深的耦合在一起了。

为了解决这个问题。按照面向对象的做法,应该抽象一个纯虚类定义接口I,moduleCx分别实现接口I。这样moduleB只需要持有接口I的引用,仅仅把数据传入接口I即可。不论增加多少moduleC,对moduleB的修改总是close的。

然而这里存在一个非常大的问题。moduleCx的每个处理接口的数据类型不同,这是对抽象出接口I的最大障碍。只有将接口的数据类型与moduleCx进行解耦,才可以顺利抽象出接口I。

对于c语言来讲,比较粗暴的做法是使用void *,对于c++的做法最常用的做法是对数据类型抽象出一个基类P,所有接口的type均继承自基类P,moduleA的返回及moduleCx的接口类型均使用基类P。到了moduleCx内定后再自行转为实际使用的数据类型。

每一种语言都有相应的思维层次。

站在c语言层面,我眼中是内存和cpu因此我觉得把其他类型转为void *,在moduleCx中再转为相应的类型并没有什么不妥。因为在我眼中不过都是同一块内存罢了。

然而站在c++(面向对象)的层次,我眼中尽是对象。每个moduleC接口的参数type之间并无共同之处,为了使用语法糖而是用继承,总是感觉心里有些许别扭。

于是我想了另外一种独立于语言的做法。

可以定义类typeX,typeX就像是c中的union一样,可以包含type1,type2 … 等moduleCx的所有类型的数据。接口I和moduleB与moduleA的返回值均使用typeX。在moduleCx中通过typeX变量获取自己需要的数据即可。

如何实现typeX使得编码更方便,则需要具体情况具体分析了。

使用多态来做到open-close

自从看了设计模式了解到open-close原则后, 我在写代码时都是尽量遵循着open-close原则来进行编码。

而面向对象中的多态在做到open-close原则中起到不可忽略的作用。

一般在设计之初会先抽象出一个interface(也就是C++中的纯虚类), 这个interface中的函数接口一定是要仔细考量的,因为这关系到所有子类的实现。

然后根据具体情况去继承并实现interface,当我们新增功能时,仅仅重新继承一下interface生成一个新的类即可, 在一般情况下并不需要动到之前运行良好的类。当然不论你interface定义的有多么好, 在最后新增需求时都可能不会完全满足,然而这种情况下一般只需对interface增加函数即可, 对之前运行良好的代码也并不需要做出大的修改。

举个例子:
一个游戏有三个模式ModeA, ModeB, ModeC。我觉得这是一种最天然的抽象了,几乎就不用思考就可以肯定,多态一定是优于if else方式的。


使用多态的方式, 首先抽象出一个interface类似如下:

class IMode {
virtual doSomeThingA() = 0;
virtual doSomeThingB() = 0;
...
};

然后根据ModeA/ModeB/ModeC的实际玩法去实现IMode类开中函数。

这样只需要在游戏切换模式时使用如下类似代码散转一下即可:

IMode *mode;
switch (game_mode) {
case ModeA:
mode = new ModeA();
break;
...
default:
assert(!"unkonwn game mode");
break;
}

在代码主体逻辑中,仅仅使用mode指针即可,即主体逻辑并不关心当前mode是ModeA/ModeB/Modec中的哪个, 他仅仅去调用相关的IMode中的函数即可。

这样当去看主体逻辑代码时,暴露出来几乎全部是主体逻辑,而不会被模式相关的东西弄晕了头脑。

当新加一个ModeD模式时, 仅需要修改上面的Switch语句加一个case另外再实现一个class ModeD类即可, 并不需要修改之前运行良好的主体代码。也就符合了open-close原则。


再看if else的实现,假设如上IMode函数有50个函数DoSomething1~50, 如果使用if else的方式实现,则需要在调用DoSomething1~50的地方全写上类似如上switch的语句,需要搞清楚的是IMode有50个函数并不意味着这50个函数仅仅调用50次(甚至说是150次都不算多), 那么就会出现代码量暴增。

而且这样会导至所有的模式代码是以函数分开的,在增加每一个模式时主体代码都要增加50个函数并修改150处switch-case语句,如果改漏一处就会出现难以预料的bug, 这样大大增加了代码的维护难度。

当然这种还算是好的情况,如果在IMode类中的函数非常短,那么在使用if else方式实现时有些人偷懒就可能会直接用case一段代码语句直接搞定,甚至在后续开发中都可能出现这样的代码:

switch (game_mode) {
case ModeA:
case ModeB:
do_something();
if (game_mode == ModeA)
do_sometingA();
else
do_somethingB();
break;
...
};

如果有4个模式这样纠缠到一块,我打赌你看到这段代码时一定会想知道他家在哪里。

这样修改还有一个坏处就是并不符合open-close原则,因为你在改ModeB时可能会把ModeA改坏,而且有时后会很难测出来。


通过比较很容易就可以看出,多态和open-close的好处,在这里我并不想听到if-else/switch-case比使用多态效率高这种鬼话,我认为损失一点点性能能获得这么清晰的代码结构是非常值得的。

在《Unix编程艺术》P14中说过这样一句话,计算机编程的本质就是控制复杂度,比较两种实现方式可以看出第一种方式,不论增加多了种模式其复杂度几乎都不会增加,而第二种方式每增加一种模式都会使得代码变得更糟糕一些,而且永不停止直到他再也无法被维护。

服务器的分布式部署

两周前使用redis-benchmark测了一下silly的并发请求响应速度, 开1000个客户端, 平均大概每秒能响应6W个请求。
但是这些请求仅仅是PING/PONG协议,也就是说这基本上就是纯网络I/O的性能,如果考虑到逻辑处理、数据压缩、加密、解密等时间, 可能一个silly都不一定能够撑得起5000人的访问量。

所以最近两周除了在研究redis之外, 就是在研究怎么给silly增加cluster支持,以便可以将计算分摊到不同的计算机上来降低客户端的请求的响应延迟,silly应该怎么增加对cluster的支持。

直到昨天下班的路上我才想通,其实对于silly的模型来讲,他的任务应该相当的KISS,仅仅只是用coroutine的方式来处理数据即可。分布式功能应该在应用层实现,并不需要silly做额外的支持。


首先给定这样一个命题:
所有玩家在同一张地图上游戏,一台物理server能支持5000人的并发量,那么如何才能支撑百万级的玩家在同一张地图上游戏。

首先将服务器分为auth_server, channel_server, scene_server, ctrl_server这四种服务器。

整个服务器集群由一个auth_server、一个ctrl_server、若干channel_server、若干scene_server组成。

所有server在启动时主动向ctrl_server建立一条socket链接, 以便可以定时向ctrl_server汇报和查询状态(如当前负载情况等),另外在停服时可以直接操作ctrl_server, 然后ctrl_server通过建立的socket连接来通知所有server停服,这样可以避免人工操作出现的各种差错。

auth_server服务器的作用仅仅用来做用户认证,和负载均衡。在客户端用户登陆验证正确后,auth_server根据向ctrl_server查询到的所有channel_server的负载状态,挑选出各适的channel_server服务器的ip及端口号,然后将其和临时通行证一并发给客户端。 当客户端得到临时通行证和相应的channel_server地址/端口号之后,随即与auth_server断开连接以便auth_server可以有更多的资源处理其他客户的认证请求。

每一个scene_server管理地图上的一部分场景,并用来处理这块游戏场景内的游戏逻辑,这样所有的scene_server组合起来的集群就是一张大的地图,和完整的游戏逻辑。

每一个channel_server与所有的scene_server相连接,并将每一个scene_server负责的区域信息与此scene_server的socket连接做hash映射,每一个客户端只会与一个channel_server相连接。

channel_server所做的事仅仅是根据当前客户端的人物的位置,自行转发到不同的scene_server服务器中去, 具体逻辑由scene_server进行处理。 当客户端在地图中行走时,如果channel_server判断超出了当前的scene_server的范围, 则根据客户端的当前位置从映射表中查出当前区域信息所在的scene_server的socket链接, 并将数据转到的新的scene_server服务器中去。

这样看来只需要一个常量的代价(数据被中转一次,hash查询),channel_server和scene_server几乎可以无限扩展,那么整个逻辑的响应速度也会是常量。惟一受限的将会是auth_server的响应速度。

ps.集群的思想参考自redis的集群,由于我并没有参与过RPG游戏开发,因此上述观点均为理论推测。

silly的socket模块重构

最初, 我仅仅最只想将silly实现成一个socket异步框架, 每一个socket有数据或事件过来直接将注册的处理函数异步回调即可。

然后, 随着三国杀的一步步实现, 我发现我之前考虑处理时漏掉了数据库环节。

由于所有socket事件均为异步, 当一个client发过来一个请求, 而这个请求需要使用到数据库数据时, 可能就会写出类似下面这样的代码:

socket.recv(fd,function(fd, data)
--process segment1
db.get(key, function(value)
--process segment2
end)
end)

这种异步代码写起来非常费劲, 而且如果通信协议设计不好, 当–process segment2还没有被执行, 下一条socket的数据包又过来, 就有可能造成错误的结果, 而且不容易发现问题所在。

这次的重构,我希望可以在上层屏蔽掉底层的异步逻辑,这样代码写起来会更清晰不容易出错。


大概设计是这样的。

每一个socket在建立链接之后, 便拥有一个coroutine, 和一个数据队列。
每一个socket数据包过来之后均使用这个socket的coroutine来处理,如果coroutine在处理数据包过程中挂起,则将数据推入此socket的数据队列中.
每个socket的coroutine恢复并处理完上一个数据包后,会检查队列是否为空, 如果队列为空则yield等待下一下数据包的到来, 否则将消耗完数据队列中的数据。

由于socket/tcp具有fifo的特性, 如果目标数据库服务器的request/response同样是按照fifo方式处理的话, 就可以实现一个socketfifo模块(如果是其他方式,只要对sockefifo稍加修改即可)。

socketfifo模块用来在每一个socket的coroutine中管理数据库socket的命令/应答,在向数据库socket发送命令后阻塞此socket的coroutine, 直到获取到数据库socket发过来的数据。

socketfifo持有一个coroutine队列。

socketfifo向数据库socket发送命令之后, 将当前的coroutine(即调用socketfifo模块的socket所在的coroutine)塞入coroutine队列中, 然后挂起当前的coroutine。
在socketfifo收到数据后,按照fifo的特性,从coroutine队列中取出一个coroutine将其唤醒并将数据返回给此coroutine。这样即可达到阻塞访问socket的目的。

虽然用同样的方法实现了阻塞connect, 但如果connect函数并非是在coroutine中调用就需要手动创建一个coroutine来将connect函数包住。


在重构过程中发送一个关于客户端主动close事件的bug。

抽象上看, 整个silly是通过两个通道来运行起来的。

socket –> worker (silly_queue) 主要向worker通知客户端的行为事件
worker –> socket(pipe) 主要向socket发送操作命令

最初设计时, 当客户端断开连接时, socket模块随即就释放所有资源,但是此时有可能silly_queue中还有此socket的数据包。
而socket模块中是有链接池存在的, 当某个socket被释放回链接池之后, 是有可能被分配给其他client的。

假设有两个socket, s1, s2。
在极端的情况下, 当s1主动断开链接时, 恰好worker还有s1的数据没有处理完,因此worker还不知道s1已经断开了,但此时s1所占用的struct conn结构体已经被释放到链接池了。
此时s2连接到服务器,恰好将struct conn分配给s2使用。

由于silly分配的socket id的特殊性,虽然s1和s1拥有不同的socket id, 但是他们都能索引到同一个struct conn结构体,如果此时处理s1数据包的函数需要向s1发送数据, 此时其实是向s2发送的数据包, 就会产生数据错乱的情况。

因此修改了一下socket关闭的策略, 当检测到client关闭socket之后,释放除struct conn结构体之后的所有资源,然后将struct conn置成一个特殊状态,直到worker收到client的关闭消息后再调用close函数,将struct conn归还给链接池。

在worker在主动关闭socket时,同样存在另外一个队列缓存问题, 当worker向socket模块发送close命令时, 并不知道silly_queue中是否还有此socket的数据,那么socket.lua模块就不知道应该什么时间释放与此socket有关的资源, 过早的释放资源容易引起其他问题。

因此在worker向socket模块发送shutdown(worker主动关闭socket命令定义为shutdown, 与tcp的shutdown无关)之后, socket模块释放除struct conn结构体之外的所有资源, 其后将struct conn置成一特殊状态,向worker回一条SILLY_SOCKET_SHUTDOWN消息, 当worker处理SILLY_SOCKET_SHUTDOWN消息时, 肯定已经消耗掉silly_queue中的与此socket有关的数据了。

此时socket.lua可以释放与此socket有关的资源,并调用close命令将struct conn结构体归还给链接池。

btw, 将多进程改进为多线程时,我曾以为可以躲避通信的复杂度, 现在看来, 当时想的太少了。 只要按照多进程的设计思路(使用队列通信), 不管是否真的是多进程, 异步通信协议的处理是必不可少的。

关于silly

自从写了第一个假server之后, 我就一直在想真正的处理高并发的server是如何做的。然后我就研究了skynet, nginx, Node.js这些服务器程序框架。

这些框架除了skynet, 另外两个框架则仅仅是从使用上进行了了解, 并没有去通读他们的源码。所以基本了解下来,skynet是一个基本actor模式,Node.js则为纯异步模式, nignx则使用了master-worker模式。

纸上得来终觉浅,绝知此事要躬行。 我觉得只是单纯的研究是不够的,还是要自己去实现一下,才会遇到并解决问题,这样才能领会到巨人的思想及解决方法。由于见识浅薄,难免有许多错误的设计, 因此定名为silly。


最初的想法是这样的:

由于最近的工作频繁使用lua, 发现用lua写起业务逻辑来非方便, 而skynet也使用了lua来作为业务逻辑编写的语言, 有大神在我也就不用怕了, 直接嵌入luaVM.

Node.js的纯异步理论看起来非常不错,他使用一个线程来跑逻辑, 然后开一个线程池来处理那些耗时的操作请求, 所有耗时操作都是通过线程池来异步完成的。

然而我一直在想,在一个纯逻辑服务器,除了网络I/O,磁盘I/O之及定时器外,应该几乎其他的事都是要通过CPU来做的,如果是这样,将那些耗费cpu的操作同样放入线程池中对于一般的服务器来说似乎意义不大(Node.js可能是要做的更统一,更通用)。即然这样,我仅仅在网络I/O,磁盘I/O及定时器上去采用异步模式,其他计算等耗费cpu的操作直接阻塞完成就好了。为了不使所有socket都因为一个socket的耗cpu操作而全部阻塞, 同时也为了更好的利用多核cpu,可以开多个进程/线程来处理socket连接。

最近一直看《Unix编程艺术》, 所以受多进程单线程的影响颇为严重,首先便选了master-worker模式来进行实现。

master与每个worker建立一条UNIX域socket以htohs(sizeof(SOCKET_FD) + PACKET_LEN),SOCKET_FD,PACKET的数据格式进行通信。

master处理的事情很简单,就是负责处理高并发连接以及数据缓存打包工作,以及以后的加密解密工作。每接到一个socket发过来的数据时, 就是去检测是否已经接收到一个完整的包,如果是则通过负载均横算法将其通过Unix域socket转发到相应的worker上去处理相应的逻辑。这样就把网络I/O剥离到master上去处理, worker就不必再关注网络I/O的问题了。

然后为每一个worker实现一个定时器线程及log线程(也许直接用syslog就可以获得速度还不错的性能, 这个有待测试),用来异步处理定时器和磁盘I/O。

每一个worker程序仅仅只需要处理一条连接,而且接到的数据一定是完整的数据包, 因此不必将cpu开销浪费在连接处理以及数据缓存合并上。这样worker逻辑仅仅处理数据包逻辑,而不心关心数据包的缓存合并问题则内部逻辑可以更简单。

而且由于大部分情况下程序都是顺序执行的, 在写代码时就可以更放心, 而不必去踩很多异步的坑。


然而随着我采用这套程序与同事搭档做三国杀之后,我发现我考虑的似乎有些少了。上而的程序结构似乎有很多缺陷。

worker上的逻辑需要连接数据库,这样以前专门由master来做的socket连接处理以前数据缓存合并的操作,就会有一部分转接到worker上做,逻辑切分就显得不那么的干净了。
worker没有办法主动请求master去关闭一个socket连接
而且之前master与worker的数据格式都是约定死的, htons(PACKET_LEN),PACKET, 而与数据库的socket协议则可能是字符串协议, 以”\r\n\r”结束, 这样势必会造成对master和worker之间的连接进行特殊处理。

想要解决这些问题势必要在woker和master之间再建立一条commad unix域socket来通信, 但是这样,会大大增加worker中逻辑的复杂度。


仔细想想,《Unix编程艺术》很反对多线程的一点就是资源以及内存的共享会使程序员不知不觉间步入深渊(死锁,资源访问冲突等)。

如果使用多进程的设计思路来使用多线程, 那么多线程出问题的概率及开发复杂度应该会大大降低。

于是一咬牙将其重构成单进程多线程模式, 将master上的功能单独用一个_socket线程来实现。将每一个worker同样使用一个_worker线程来实现。同时将之前所有worker上的timer合并为一个timer

重构后在数据的缓存和解包上做了一点小小的改变,为了解决不同连接可能有不同数据包格式的情况,_socket线程在接收到数据之后不再缓存解包,而是直接将数据转发到lua中相应的数据包处理函数中, 由业务逻辑来选择数据包的解析方式, 这一点其实是抄的skynet的做法。

btw, 即使我已经尽力去使用多进程的设计思想, 但是还是避免不了加了一些锁, 所以整个程序依然看起来比多进程要复杂的多, 但是这些锁正是为了解决“要在woker和master之间再建立一条commad unix域socket来通信”的问题,
所以相比而言, 重构后整个程序的复杂度, 还是要低于解决上述多进程的缺陷之后所带来的复杂度。

代码模块化(二)

在按照上一篇的做法将代码重构之后,发现虽然模块之间的依赖关系虽然明确了,
但是使用方式2的模块太多了, 方式2有一个弊端就是,对于模块间的引用没有天然的限制,如果不小心使用了某个模块却没有调用此模块的init函数,在某些情况下也是能够正常运行的。

因此我决定将除了log还要config这种几乎所有模块都会用到的模块使用方式2外,其他的模块全部使用方式1来实现。在实现之初我觉得除了那些最基础的模块外完全采用方式1来实现应该是一个完美的树形,但是在实现过程中发现将模块化的组织为一个完美的树形有着种种障碍,同层模块之间的交互几乎不可能全部在上层模块中解决,在这一个月中我一直在各种架构书籍中去寻找答案, 得到的结论竟然是想将代码写成完美树形是不可能的.

我甚至于找到两个很常见的例子来推翻可以将程序组成完美树形的依据, 一个公司的结构与一个PC机的结构:

公司的结构其实理论上来看是个完美树形, 老板下面有一堆经理, 每个经理下又有一堆组长. 但是仔细思考后就会发现, 如果有什么事, 老板肯定不会去了解所有细节而是让所有经理来开会, 换句话说, 老板几乎不可能先去与A经理商议再将得到的结果与B经理商议. 肯定是招集所有经理来开会, 那么经理之间就会有直接交互,这是不可避免的, 由于同层人员有交互那么其依赖关系就不可能形成一个完美的树形

PC机主要的三大件CPU, 硬盘, 内存的交互仔细想来也并非我所想象的完美树状依赖关系, 如果CPU想将硬盘的内容读到内存里去, 只是与硬盘发送读命令, 向内存发送写命令, 其数据传输是需要内存与硬盘的总线来进行的, 并不经过cpu, 这同样与我想象中的硬盘与内存所有交互是通过cpu来进行并不相符.

在上面的实例中我得出一个结论, 上层代码会控制其直属下层代码之间进行交互,但是并不是所有的交互细节都可以通过上层模块来进行(当然同层模块之间的交互不能够形成循环依赖),在有些必要的情况下,两个同层模块间必须要留有接口用于交互。

经过不段斟酌,我认为代码模块之间的同层交互是这样的:
假设有三个模块module_a, module_b, module_c

module_a 属于上层模块, 其直属模块为module_b, module_c.
module_c与module_b处于同层, 但是有一个操作module_c 需要借助module_b来操作, 那么module_c的接口可能需要如下定义:


//module_c.h

struct module_c;
struct module_b;

struct module_c *module_c_create();
int module_c_free(struct module_c *m);

int module_c_do_something(struct module c *m, struct moduel_b *b, ...);

个人认为其实这种的同层模块间交互还是要尽量避免的, 如果两个模块之间有了接口依赖, 那么两个模块的耦合度就会增加, 其中一个模块就要对另一个块的接口有所了解, 对于维护和替换模块是不利的. 而如果去画依赖关系图的话, 也会增加一道依赖关系, 设计清晰度也会下降.

代码模块化(一)

今天重新review了一遍代码, 发现模块竟然有几十个之多, 之间引用大概如下:

MODULE_MAIN -> MODULE_A MODULE_B MODULE_C …
MODULE_A -> MODULE_B MODULE_C MODULE_SUB_A1 MODULE_SUB_A2 …
MODULE_B -> MODULE_SUB_B1 MODULE_SUB_B2 …
MODULE_C -> MODULE_SUB_C1 MODULE_SUB_C2 …

也就是说这些模块之间其实并非是同级的, 有些模块只是被某一个上层模块调用罢了.
但是我目前代码里面其实并不能很明显的看到这种引用关系, 如果这种模块多了, 相互间的引用关系在大脑中就容易出现混乱(最起码我是这样), 而且不容易维护.

因此必须要尽最大可能明确这种依赖关系, 并加以限制以使模块之间的依赖不会循环不会交错.

我一般是一个C文件来当做一个模块, 如果一个C文件过长就会将此C文件拆成几个子模块.

总结一下目前我使用的模块创建方式:
//方式1
struct module;
struct module *module_create(…);
int module_do_something(struct module *self, …);

int module_free(struct module *self);
//方式2
int module_init(…);
int module_do_something(…);

方式1由于整个module相当于存在于self所指向的内存区域, 以上面的引用关系来看MODULE_SUB_A1之类的模块很适合采用方式1来编写, 因为此模块只是为更好的完成MODULE_A, 那么也只有MODULE_A才能来使用, 如果使用了方式1, module_create返回的self就只有MODULE_A才能拥有, 就不用担心被其他模块引用, 或错误的引用.

在写代码时应该很少会遇到所有模块只会被引用一次, 一定有些模块用来当作基础模块来使用, 如上面的MODULE_B, MODULE_C这种即被MODULE_MAIN引用又被MODULE_A引用, 那么此时MODULE_B, MODULE_C再使用方式1来实现就势必会出现全局变量了, 全局变量的出现会使程序变得更糟糕, 这时就必须使用方式2了.

但是为了让方式2也能清晰表示出依赖关系, 可以稍加限制来清晰表明模块引用关系, 示例代码如下:

module_a 引用 module_b
module_c 引用 module_b

int module_a_init()
{
….
module_b_init();

return xx;
}

int module_c_init()
{
…..
module_b_init();

return xx;
}

如果每个模块都遵循这种规则, 那很就可以很清晰的看出模块之间的依赖关系. 这时会出现一个问题就是module_b_init可能会被多次调用的问题. 可以在自己模块数据中记是否被init的过来实现, 也可以通过实现一个module_init_once(int (*init)(void))函数来间接调用module_b_init, module_init_once来保证module_b_init只会被调用第一次.

————————————
最近越来越觉得其实代码模块组织应该是树形的.

注:部分灵感来自云风大神

无锁编程

好久没写blog, 最近都在研究无锁算法, 一直在纠结为什么无锁高效, 无锁是否是真的无锁.

看了coolshell的无锁队列后, 曾经专门做过锁的性能测试. 自以为发现了无锁的高效之处.

可是真到我的无锁队列写出之后, 突然间感觉之前的测试太片面了, 无锁对于效率的提升也许并没有我想象的那么高, 也许并非无锁
当然其效率提升其实已经很显著了在某些条件下.

再回过头看重新看了一下coolshell中对的无锁队列下面的那行总结”如果不用专门的车锁, 那么就得自己锁自己”, 突然间发现大牛的观点与我一样.

下面看一段coolshell上的代码:

EnQueue(x) //进队列
{
//seg1
q = new record();

//seg2
q->value = x;
q->next = NULL;

//seg3
do {
p = tail;
} while( CAS(p->next, NULL, q) != TRUE);

CAS(tail, p, q); //置尾结点
//seg4
}

从代码上分析看出, 整个函数可分为3段:

seg1~seg2之间其实肯定是有锁的, 因为分配内存函数必然有锁.
seg2~seg3之间是可以并发执行的, 如果有2核的cpu同时跑2个线程那么这一段代码是可以并行执行的.
seg3~seg4之间作用也与锁等价, 在这段区间内如果cas失败了, 必然会引起线程切换, 否则这个线程永远也不会成功(同时有3个线程执行到seg3之后, cpu只有两个核的情况下).

如果EnQueue写成有锁的话那么seg1~seg4是全段加锁的.

所以比较一下有锁与无锁的本质来看, 可以看出其实虽然是无锁编程虽然号称无锁但其实也是有锁的, 只不过是换了一种形式的锁而已.
从锁的范围来看, 可以看出无锁编程中锁的区域更小, 线程之间碰撞概率更小, 那么引起线程切换的概率会更小, 线程切换带来的开锁会更小.

软件日志

在一个健全的软件中,日志系统是必不可少的,因为软件发布到各个地方你并不总是有条件去下断点然后去调试。而且如果你的软件中大量使用多线程,那么即使你有条件断点恐怕也是无济与事,因此最古老也是最有效的方法就是为软件实现一个健全的日志模块。

但是日志过多是会影响效率的,日志过少出问题时又难以用于分析。因此我们可以为日志分级。日志模块可以导出接口如下:


int log_init(void);
int log_set_level(int level);
int log_add(int level, const char *format, ...);
int log_free(void);

日志分为0, 1, 2级.
0级log只打印出导致函数执行失败情况下的信息.
1级log除了打印0级log外还要打印出关键函数内部的执行情况及其他对于软件整体运行至关重要的执行信息.
2级log除了包括0级和1级log之后要打印出每个函数的参数及最终执行结果.

日志级别写入软件的配置文件中, 而不要在程序中使用log_set_level写入一个固定值, 那么当发布给客户时默认级别为0级, 如果用户使用过程中碰到我们未发现的异常, 我们可以根据情况, 修改配置文件中的日志级别, 当现象再次重现时, 我们就可以从日志上得到足够的信息来Debug.

当然这跟断点调试还是有很大差距的, 所以平时写代码不能过度依赖调试器, 有时候也要练习一下人脑debug, 这也要求我们在写代码时一定要考虑全面思维严谨.

代码正交性的一种实现

《unix编程艺术》里面讲到在写代码时尽可能的让代码保持正交性, 但是其实在有些情况下正交性也不是这么容易达到的, 下面举例两个函数, 虽然功能不是很实用, 但是表现的现象在项目中还是经常会碰到的:

int sum_a(const int a[], const int b[], int cnt)
{
int i;
int sum;

sum = 0;

for (i = 0; i < cnt; i++) /*--------------------------line1--------------------------*/ sum += a[i] + b[i]; /*--------------------------line2--------------------------*/ return sum; } int sum_b(const int a[], const int b[], int cnt) { int i; int sum; sum = 0; for (i = 0; i < cnt; i++) /*--------------------------line1--------------------------*/ sum += a[i] / 2 + b[i] / 2; /*--------------------------line2--------------------------*/ return sum; }

从上面可以看到其实这两个函数从结果上来看做的完全不同的事, 但是其接口是一样的, 甚至连大部分代码也是一样的, 最关键的是他们不一样的代码是在line1与line2之间, 这样便不可以把line1之上的代码封装一个函数,因为line1之上的代码与line2之下的代码是有很强的关联性的. 因此我惟一能想到的还是回调函数. 将代码改为如下:

static int handler_a(int a, int b)
{
return a + b;
}

static int handler_b(int a, int b)
{
return a / 2 + b / 2;
}

static int do_sum(const int a[], const int b[], int cnt, int (*handler)(int a, int b))
{
int i;
int sum;

assert(handler);

sum = 0;
for (i = 0; i < cnt; i++) sum += handler(a[i], b[i]); return sum; } int sum_a(const int a[], const int b[], int cnt) { return do_sum(a, b, cnt, handler_a); } int sum_b(const int a[], const int b[], int cnt) { return do_sum(a, b, cnt, handler_b); }

从上面来看改为了正交性之后其实通用性更强, 如果有很多类似sum_a, sum_b之类的函数的情况下, 代码量会明显降低, 但是如果如例子所示只有sum_a, sum_b函数的话, 这么做代码将明显增多, 由代码行数与bug成正比关系得出这其实是不划算的, 另外这种做法完全依赖于编译的优化能力, 如果编译器不进行优化的话在调用handler函数里会压栈很频繁, 对于执行效率来说这么做是不可取的, 幸运的是我测试了几次发现编译器对于这种函数指针的调用的会优化的, 尤其是当函数指针传参为常量的情下.

另外可以看出函数的正交性其实是有一个度的, 这个度没有一个固定标准, 只能靠自己的经验去衡量取得最佳值.