在日常编程中,很多程序由多个小一点的部分组成。这个程序运行这些小的部分,获得它们的结果,并用这些结果来计算出整个程序的结果。很多算法也是这样的,一个算法有几个步骤,每一步的结果都对最终结果有影响。一般来说这样的程序和算法都是顺序型的,除非是专门的并发算法,或者是有时间要求的高性能程序。
如果要你去编写或重构一个这样的顺序程序,但又包含并发的部分,你会需要什么样的编程技术呢?现代的语言象Java和C#,都有内置的线程支持。但问题是线程是一种低级(接近机器硬件)的技术,大部分时间经常是花在改程序结构来容纳线程,而不是让线程来为你的程序服务。
幸运地是Java和C#大约从2005开始提供更完善的编程技术。比如Java提供了 ExecutorService,Callable,和 Future。C#提供了
IAsyncResult 设计模式。
Java: http://docs.oracle.com/javase/tutorial/essential/concurrency/exinter.html
C#: http://msdn.microsoft.com/en-us/library/ms228963.aspx
本文的重点是通过一个例子来说明 FastMessenger 如何帮助程序员将顺序程序的部分重构成并发的,所以不会涉及Java和C#的方案,也不涉及将 FastMessenger 和它们比较。这些都留给以后的文章再谈。
本文用到的例子 Fibonacci 是从一本计算机教科书里来的,书名是“Introduction to Programming in Java”。这本是Java书,但不表示我在这里谈的光是Java的东西。实际上 FastMessenger (implementation)自己在Java,C#和JavaScript都已经实现了。你完全可以用这里谈到的技术去改写你的C#程序。
教科书: http://introcs.cs.princeton.edu/java/home/
源程序: http://introcs.cs.princeton.edu/java/92symbolic/Fibonacci.java.html
两个大的BigInteger相乘是非常慢的,所以我的目标是让这个例子里的乘法并发运行。我将改写源程序中的这句。让这句里的两个乘法并发执行。
BigInteger result = fibonacci(n2).multiply(fibonacci(n2)).add(fibonacci(n3).multiply(fibonacci(n3)));
这句太长了,在重构前,我把它分开了来写,让它更易读。
BigInteger Fn2 = fibonacci (n2); BigInteger Fn2xn2 = Fn2.multiply (Fn2); BigInteger Fn3 = fibonacci (n3); BigInteger Fn3xn3 = Fn3.multiply (Fn3); BigInteger result = Fn2xn2.add (Fn3xn3);
现在开始用 FastMessenger 来重构了。第一步是写一个新的worker class,它要做的就是两个BigInteger的乘法。
public class FastMessengerWorker { public BigInteger multiply (BigInteger a, BigInteger b) { return a.multiply (b); } }
第二步是在主程序 Fibonacci 里加一个 FastMessenger。因为源例用的是 static method 的,我也只好使用 static 了。FastMessenger 使用一个称为“中间人”的主结构,呼叫方(caller)和被呼叫方(callee or receiver)只能通过这个中间人来进行。除了 FastMessenger 的 instance,第二步的代码里还建立并注册了两个 worker。
private static IMessenger messenger; static { FastMessengerWorker[] workers = new FastMessengerWorker[] { new FastMessengerWorker (), new FastMessengerWorker () }; messenger = new Messenger (); messenger.registerReceiver (workers, "worker", "multiply"); }
最后一步,改写原程序,让它通过 FastMessenger 来让两个 worker 干活。
BigInteger Fn2 = fibonacci (n2); IResponse<BigInteger> Fn2xn2 = messenger.sendRequest ("worker", "multiply", Fn2, Fn2); BigInteger Fn3 = fibonacci (n3); IResponse<BigInteger> Fn3xn3 = messenger.sendRequest ("worker", "multiply", Fn3, Fn3); BigInteger result = Fn2xn2.get ().add (Fn3xn3.get ());
和前面分行的原程序比起来,行数和结构都没有变化,但却变成并发的了。FastMessenger 为每一个 caller 的 request 都返回一个 IResponse。一方面这个 caller 继续执行下面的语句,另一方面,FastMessenger 开始调用一个 worker 来做乘法。那 caller 何时何地接受乘法的结果呢?就在 caller 调用 IResponse.get() 的那时那一行。这个 get() 是阻塞式的,要等乘法真的完成了有了结果才会返回。
这里将列一下这个例子里涉及到的 FastMessenger 的功能和特性(1.4版后)。下载地址在http://java.net/projects/fastmessenger/downloads。
Receiver 里保证是单线程模式
Receiver首先要在FastMessenger注册,然后FastMessenger会根据收到的request来调用一个receiver instance来处理这个request。FastMessenger将保证在一个receiver instance返回之前不会再次重入调用。
内置的负荷平衡
一个receiver是由注册时的 id 来区分的,一个 id 一个receiver,但一个 id 可以有一个以上的instance。因为一个instance一次只能处理一个request,所以当所有的instance都忙的时候,多余的request就只能等待了。
使用reflection来提供参数化的message以及用method names代替message ports
FastMessenger 1.4 以前的版本只支持 IReceiver 的sub class。那样的话有点不方便,因为method signature是固定的。最近我为了易用性,加了reflection,虽然损失了编译时的类型检查但至少让用户多了一个选择。从上面的代码,你们一定会同意真是太方便了。
可定制的返回类型
和参数类型一样,返回类型也是可以定制的,所以当caller调用 IResponse.get() 的时候,显得非常好用。