Archive for the 'Design Pattern' Category
基于文件系统的生产者和消费者问题
周末的时候和team讨论了下如何用最简单的方式,提高数据文件的单位时间传输吞吐量。下面是一个简单的应用场景:
一个目录(DIR1),有很多Producer向这个目录里面放文件,同时有很多的Consumer负责从这个目录里面消费这些文件,插入数据库或者做其他的操作,然后删除或者移走这些文件。
假设条件:
- 一个文件中转目录DIR1,这个目录位于一个网络存储上
- 一个生产者,每一秒钟向DIR1里面放一个文件
- 若干个消费者,假设有8个,其实是8个不同的服务器,都可以访问DIR1,多台服务器可以起负载均衡的作用,任何一台或者几台出问题,整个数据流不会中断
- 解析一个文件大约需要2-14秒
- 最后一点:位于网络存储上的目录DIR1,我们认为它是不会出问题的,它不是这里的问题核心
这个场景很普遍,很多公司大概都会用到,尤其是那么比较老的系统(Legacy System),下面是两种方案:
方案一
Consumer循环扫描DIR1,一旦发现有文件,循环解析这些文件,这里有8台服务器,也就是说有8个Consumer一起这样做。代码如下:
public void run() {
System.out.println("Created consumer:" + threadName);
while (true) {
File file = new File(Constant.STAGING_FOLDER);
File files[] = file.listFiles();
for (int i = 0; i < files.length; ++i) {
File f = files[i];
parse(Constant.STAGING_FOLDER + "/" + f.getName());
}
Commons.sleep();
}
}
看起来很简单,可是上面的代码效率非常的差,多个Consumer有很大的几率拿到相同的文件,当某个Consumer尝试去解析一个文件时,却发现这个文件已经被别的Consumer解析过了,并且文件也都删除或者移走了。这样浪费的很多的CPU时间。
可以用下面的方案来替代:
方案二
public void run() {
System.out.println("Created consumer:" + threadName);
while (true) {
File file = new File(Constant.STAGING_FOLDER);
File files[] = file.listFiles();
int nCapacity = files.length > Constant.CAPACITY ? Constant.CAPACITY
: files.length;
System.out.println(this.threadName + " found " + nCapacity
+ " files");
for (int i = 0; i < nCapacity; ++i) {
File f = files[i];
f.renameTo(new File(Constant.TMP_FOLDER + "/" + f.getName()));
}
for (int i = 0; i < nCapacity; ++i) {
parse(Constant.TMP_FOLDER + "/" + files[i].getName());
}
Commons.sleep();
}
}
它和方案一的不同之处在于:它每次扫描完目录后,最多只取前若干个文件,这里是10个。并且,它不急于去处理文件,而是把文件马上移动到一个临时工作目录,其他的的操作都是相同的。
对于这个方案,有个附加条件:这个临时工作目录tmp,一定要和staging目录在同一个文件系统(filesystem),这样的话,mv操作就只是修改一下inode,几乎瞬间完成。
比较(Benchmarking)
为了测试两中方案的效率差别,我写了一个模拟程序(http://googlestop.com/download/SimConsumer.7z),它有7个class:
- App.java - 程序入口
- Commons.java - 共享的函数
- Constant.java – 配置参数
- Producer.java - 生产者,每隔一秒向目录staging里丢一个文件
- AbstractConsumer.java – 抽象消费者,定义消费者的一些基本属性和行为
- Consumer1.java - 具体消费者,实现方案一
- Consumer2.java - 具体消费者,实现方案二
在App.java中,你可以指定调用Consumer1还是Consumer2。
对于前者(Consumer1),staging目录下的文件数目不停的增长,并且如log显示,有很多冲突:一个Consumer准备处理的文件已经被其他的Consumer处理完了,造成了很多无效的操作,由于消费速度更不上生产速度,DIR1被撑爆只是时间的问题。
对于后者(Consumer2),staging目录下的文件几乎马上就会被移动到tmp目录下,大部分时间,文件数都为0。而tmp目录下,在程序稳定后大概保存在20多个文件左右,保持一个动态的平衡。用这种方式,你也会看到很多冲突,但是只会发生在程序刚开始,原因是,刚开始的时候,8个线程几乎是同时去访问staging目录,势必拿到很多相同的文件,待到稳定后,就很少有冲突发生了。
这两种方案都是最基本的,没有借助于第三方工具完成的,成本是最低的,其实还有一些其他的方案,可能会借助一些服务来实现,比如消息分发、数据库等。有时间的话,我继续补充。
建立自己的消息循环
在MFC程序中使用消息机制的时候,常常会在程序即将完成的时候发现,在某个窗口中加入了太多的消息处理宏,并且这些消息的Handler和UI界面的方法混合在一起,待到项目规模大的时候,会导致后续的维护越来越困难。我们希望将UI的动作和业务处理的动作隔离开,互不影响,我们可以模仿MFC的消息流转机制,就本文来说,是Command的流转(Command Routing),大家知道,所有的从CCmdTarget派生的类都可以支持消息处理,比如CDocument, CView, CDocTemplate等。Framework在内部使用了Chain of Responsiblity。消息会在一个链条上找到自己的执行的地方,我们可以把不同的业务逻辑分割为各个不同的环,我们可以在修改一个环的情况下,而不去影响其他的环。附件中的例子只是简单的发送一个WM_COMMAND消息,消息的ID,表示了不同的动作。此方法适合发送消息时候,参数很少或者不用参数的情况。
示例代码的简单说明:
- 业务处理类需要从CCmdTarget派生
- 在每个业务处理类里面加入适当的消息映射(注意更新MyCommand.h中的消息的宏)
- 在CCmdManager类的BuildCmdTarget()中加入类的初始化
- 在主窗口的OnCmdMsg中调用 CCmdManager的CallOnCmdMsg方法
P.S.
这里写的只是MFC消息机制最粗糙的改造,由于时间的关系,这里不做过多的介绍
示例代码,点击这里下载
Here are some links about design pattern on the web
http://www.dofactory.com/Patterns/Patterns.aspx
http://home.earthlink.net/~huston2/dp/patterns.html
If you’re new to Design Pattern, the links listed above may be helpful. the first one is easy to understand. it shows you the skeleton of every pattern and illustatues the details in real-world code. by the way, <<Head First Design Patter>> is also an amazing book for us, it’s the best seller in many online bookshops recently.
Observer设计模式之软件语言的动态切换
随着网络的普及,软件的国际化问题,越来越值得让软件作者们多花些时间去思考。下面结合Observer模式,给出一个简单的多语言切换的例子。