不要当文档系统的奴隶

2012-02-27  邓智群 

无论你是从事小规模的应用程序开发或者是你已经开发了企业范围的应用程序,但是在某一时刻你可能会遇到需要读取文档的情况。这会使你沉浸在看java.io包的喜悦中。虽然包本身是被精心设计和结构化的,但是它不能解决根本问题——事实上你正在对付的是一个文件系统。你的应用程序的反应时间会被文件系统本身的反应时间所限制。

  对于大多数的本地文件系统都不是问题(即使你有繁忙的文件系统和一些大容量的读取,你可以开始去体验它!),但是像NFS或是mapped drives就成问题了。许多带有非常旧的遗留系统的集成是基于网路入径上的文件交互。特别是酒店业(酒店,酒吧和饭店)都是这样的自上世纪80代就未曾改变过的系统。在这种情况下即使你一次想交互1KB的数据,因为你正在处理一个远程文件系统并且大多数情况下这个系统都是很忙碌的,从50到100这样的"message files"每分钟都在交换,然后通过网络传播。你偶尔会看到读取这样小的文件要1-2秒钟。

  在平常的web-based的应用程序中这可能不是问题,因为用户可以等待(但是,即使在这种情况下态度已开始转变!);然而,如果用InputStream.read() 用于你的应用程序来代替等待将是一件非常好的事情。你还可以同时做其他的事情。确保能够改变你的应用程序执行运行流畅这样你就只能读取。在这种情况下,用户体验只在最后一步——在你已经提供了非常平稳快速的体验之后。但是,有时候无法做到这一点,即使做到了也不能解决问题。但是,背后隐藏的事实就是大多数的用户行为是及时做出反应,因此,最后用户也会容忍这样的"小"故障。

  主要的问题是由于在你的应用程序中有像这样的一些代码:

        //open the file
  FileInputStream f = new FileInputStream( file );

  ...

  //read, possibly repeatedly

  f.read( bytes );

  ...

  //finished, close the file

  f.close();

  这个问题源于的事实就是所有那些操作正在阻止运行,最重要的是read()。因为它正在阻止运行,在你的代码中你无法做任何事情除非一直等它返回。除非上述阻碍是发生在一个单独的线程!

  这就是本文主要中心思想的源头:有一个"framework"岂不是很好,它可以在后台为你做读取,同时你可以做别的事情,当它结束读取时会提示你,所以你最后能用上这些文档读取的内容了。虽然这并没有消除在读取文档时的延时,但是它将大大减少在应用程序中等待的时间——因为在读取工作进行时,在应用程序中你还做了其他的工作。

  这部分的构架是非常简单的。你需要构建一个列队,在这里“clients”可以为文档读取来添加需求,然后"read manager"会从这个列队中选取一个条目并且产生一个线程来执行实际的读取。当结束读取后,这个线程将会通知在列队中放置需求的客户端并且通过作为一组字节的该文件的内容。

  当然,我们来做一些假设:

  · 文件内容被储存在一个byte数组(byte array)中。即使这样我也仍看到应用程序在读取超过2GB的文件!

  · 读取该文件只需要花几秒中。如果有一个文件用几分钟来读取那一定是出了什么差错,尽管你仍然使用这部分。除非你的应用程序中有工作是需要几分钟来完成的,那看是去你是要得到比较好的结果。(不要提那些必须去等待好几分钟的水平差的用户!)

  · 你的系统在不放慢应用程序的情况下选中更多的线程(threads)。

  在大多数情况下你不必去担心以上所述(如果你开始担心了,就是时候去重新画板了!)。我想做这些假设是非常好的。

  ReadCallback –这是一个界面用于当文档读取成功完成时,通知将文档读取要求放在列队中的用户,在这种情况下我们通过执行readingFinished()来执行字节读取,或者一些错误发生时我们通过readError()来通知用户关于所遇到的错误并且通过exception object。当然其他的通知也可以添加到这个接口中来如果有必要的话——像fileOpened, fileClosed,和bytesRead。以我的经验来看,就上述所描述的你所要关心的是读取文件的内容,所以我认为没有必要在添加这些……。
ReaderThread –这是一个级,通过FileInputStream用以上所述的方法来完成文件读取。因为读取本身已经不是本文的主题,所要我选择简单的把所有的读取字节添加到ByteOutputStream。值得注意的是这已经不是最佳选择因为它涉及了每次读取时被用于这个级别的不断增长的内部byte数组,导致内存不断的重新分配。这个级通ReadManager线程运行到一个单独的线程中(这就是为什么实现了Ruunable界面)。就像以上所提到的它产生了两个通知的其中一个,读取完成 (reading completed)或是读取失败(reading failed)。这取决于在读取文件内容时是否遇到任何I/O的问题。

  The ReadManager- 最后一个也是最重要的,ReadManager。这是一个级可以掌握和掌管所有带有被用户放置的所有读取文件要求的内部列。在内部该级别有一列是用户用来添加他们的文件需求的。同时这个可以通过一个常规的Queue class来实现。我已经选择了BlockingQueue所以“等待任何元素被添加到列队中”的任务将会传递给实施级而不是你来完成这个任务。因为ReadManager级从主应用程序中以不同的线程来运行,直到列队中的条目都可用了,take()操作被阻碍也是没有关系的。

  如果你需要在ReadManager class执行其他操作当列表被写入时候,很简单的用Queue代替带有poll()的take()呼叫。你会注意到这个级是在无限循环中运行的。这纯粹是为了一些简单的原因。在一个“live”的环境中你可以为线程的顺利完成设计一些信号机制(因为你将注意到,主应用程序通过System.exit()存在迫使ReadManager thread通过InterruptedException来完成)。因为所有的读取要求都要通过这个部分,这已经被独立了。这个级只有一个例子——因此只有一个列队——服务整个应用程序。作为最后一个说明,你会发现每一个文件需求都产生一个新的线程。这还不是最佳的,因为一个有数以百计的文件需求的应用程序可以终止生成数以百计的这样的线程,这样就会使系统瘫痪。在一个"live"方案中,在ReaderThread.周围需要一个线程池(thread pool)。

  为了我们“客户”的应用程序,我提供了一个简化的级的执行,可以做一些用于平行读取的“lengthy”操作。在这种情况下我选择做一些随机的双倍乘法大约100,000次,但是就像我说的,这仅仅是模拟一个"lengthy"操作。所以这就真的需要你的想象力了。显然客户是要执行callback界面并且进行相应的接收通知。你应该记住,这些通知是从一个单独的线程发出的——特别是从ReaderThread中发出——因此重点记住的是这些通知一到达,主要的客户线程也在运行中。有些同步是非常必要的!这样,因为我存储了一个旗帜来说明读取已经完成(readFinished member)并且如果读取成功,数据组将会包含文件内容,或者在错误的情况下被设置成无效(fileContents member), 当这些操作进行时也会有一些同步的情况涉及进来。为了这个练习的目的,当写入或是读取这些值时,客户端代码也同步于ReadClient object本身。我相信你能找出并且针对你的应用程序来进行更改。

  把这个信息考虑进去,client flow 是非常简单的。一旦接收一个文件名需要在命令行读取,它用manager来排列一个文件的读取请求,然后执行"lengthy"任务同时read manager在后台执行读取。一旦lengthy 操作结束,就会确保文件内容可用。为了这一点可能有时候需要等待(但是希望把读取文件放在第一位的情况多一些)。一旦readFinished被设置就意味着两件事情:文件内容将会在fileContents member中或者读取失败并且fileContents无效。

  总结语

  所提到的解决方案是非常基础的一个模块的执行,这个模块允许你的应用程序使用标准的java.io package来执行“异步”文件读取。这个可以延伸到文件的写入和其他的I/O操作,如网络通讯的邮件的发出/接收。正如所指出的那样,因为原因很简单,所以很多事情都被忽略了。因为有表明这个模块是不能扩展的框框,但是小的改变还是需要的,在整个文章中也有说明。虽然这个模块只提供了基本的文件读取通知,但是在callback界面中可以延展更多的通知这取决于应用程序的需要。

  一个可能的延展就是这个小小的framework可以添加一些更多的通知到callback界面中只要字节被读取了,这样应用程序可以执行从可用的文件中执行数据读取,然后将他们转移到有意义的数据中(如SAX-like manner)。

  最后,虽然这并没有消除所发生的延时用减慢装置,像网络驱动,但是它允许程序员去孤立一个封锁操作到单独的线程中,这样就减少了一个需要重复读取的应用程序的反应时间。
389°/3896 人阅读/0 条评论 发表评论

登录 后发表评论