讨喜的隔离可变性(五)同时使用多个角色

简介:

通过前面的学习,我们已经了解了如何创建角色以及如何给角色发送消息,下面让我们来一起学习如何让多个角色协同工作。在第2章中,我们创建了一个统计给定区间内所有素数的程序。在该程序中,我们使用了ExecutorService、Callable、Future以及其他差不多超过一页纸那么多代码。本节我们将会学习如何用Akka角色对该示例进行重构,并且根据之前的惯例我们的介绍顺序还是先Java后Scala。

在Java中同时使用多个角色

假定待统计数字集合中的数字是1千万个,为了统计其中的素数数量,之前我们是将数字集合划分为若干个不相交的子集合,并将这些子集合丢给一些线程去执行统计操作。但这里我们将使用角色来完成同样的功能,下面就让我们从角色的onRecevie()函数开始说起吧:


1 public class Primes extends UntypedActor {
2 public void onReceive(final Object boundsList) {
3 final List<Integer> bounds = (List<Integer>) boundsList;
4 final int count =
5 PrimeFinder.countPrimesInRange(bounds.get(0), bounds.get(1));
6 getContext().replySafe(count);
7 }

为了统计给定区间内的素数数量,我们需要指定区间的上下限。在本例中,onReceive()函数的参数是一个List,其中前两个元素即为区间的上下限。在onReceive()函数内部,我们调用了PrimeFinder类的countPrimesInRage()函数来统计区间内的素数数量,最后又使用replySafe()函数将统计结果返回给调用者。

在给定了待统计的数字集合之后,我们需要将其划分成若干个不相交的子集合并将统计这些子集合中素数数量的任务委托给各个不同的角色来执行。下面就让我们在静态方法countPrimes()中实现这些逻辑:


01 public static int countPrimes(
02 final int number, final int numberOfParts) {
03 final int chunksPerPartition = number / numberOfParts;
04 final List<Future<?>> results = new ArrayList<Future<?>>();
05 for(int index = 0; index < numberOfParts; index++) {
06 final int lower = index * chunksPerPartition + 1;
07 final int upper = (index == numberOfParts - 1) ? number :
08 lower + chunksPerPartition - 1;
09 final List<Integer> bounds = Collections.unmodifiableList(
10 Arrays.asList(lower, upper));
11 final ActorRef primeFinder = Actors.actorOf(Primes.class).start();
12 results.add(primeFinder.sendRequestReplyFuture(bounds));
13 }
14 int count = 0;
15 for(Future<?> result : results)
16 count += (Integer)(result.await().result().get());
17 Actors.registry().shutdownAll();
18 return count;
19 }

在确定了每个子集合的范围之后,我们会将其包装在一个不可变集合里——请记住,所有的消息都必须是不可变的。接下来,我们调用sendRequestReplyFuture()这个非阻塞函数来将统计请求发送给各个角色进行处理。在把请求发送出去之后,我们将sendRequestReplyFuture()返回的Future对象(注意这里是akka.dispatch.Future而不是JDK中的java.util.concurrent.Future)保存在一个数组中以便稍后从其中取回各个子集合的统计结果。在任务分派完毕之后,我们就可以循环查询每个Future,即先调用Future的await()函数,待await()函数返回之后再调用其返回值的result()函数来获取一个Scala的Option实例——你可以将其假想为一个包含统计结果的数据单元(如果数据存在的话)。最后我们可以通过调用该实例对象的get()函数来得到一个Integer类型的统计值。

OK,下面就让我们写一个用来检验上述代码的测试用例,其中的待统计数字和子集合划分数是通过命令行传给程序的:


01 public static void main(final String[] args) {
02 if (args.length < 2)
03 System.out.println("Usage: number numberOfParts");
04 else {
05 final long start = System.nanoTime();
06 final int count = countPrimes(
07 Integer.parseInt(args[0]), Integer.parseInt(args[1]));
08 Working with Multiple Actors • 179
09 final long end = System.nanoTime();
10 System.out.println("Number of primes is " + count);
11 System.out.println("Time taken " + (end - start)/1.0e9);
12 }
13 }
14 }

main()函数主要负责对上面的统计代码进行测试并记录执行耗时。最后我们还需要实现PrimeFinder这个真正负责统计工作的类:


01 public class PrimeFinder {
02 public static boolean isPrime(final int number) {
03 if (number <= 1) return false;
04 final int limit = (int) Math.sqrt(number);
05 for(int i = 2; i <= limit; i++) if(number % i == 0) return false;
06 return true;
07 }
08 public static int countPrimesInRange(final int lower, final int upper) {
09 int count = 0;
10 for(int index = lower; index <= upper; index++)
11 if(isPrime(index)) count += 1;
12 return count;
13 }
14 }

令待统计区间为[1, 1000w]、划分的子区间为100个,则上述示例程序的输出结果如下所示:


1 Number of primes is 664579
2 Time taken 3.890996

下面让我们将本节的代码和输出结果与第2.4节的示例代码和输出结果进行比较。虽然两个版本都将子集合数设为100,但Akka版本的示例代码无需显式设定线程池大小。此外,由于这是一个计算密集型问题,所以对于使用ExecutorService的版本而言,其线程池大小的设定是需要随机器CPU核数计算而定的,所以两个版本的性能都差不多,而Akka版本在代码的形式上要比使用ExecutorServer的版本简洁一些。但正如我们在本章后面将会看到的那样,当我们需要让多个线程/角色相互协作的时候,这些区别将会愈发明显。

在Scala中同时使用多角色

如果用Scala来实现这个统计素数数量的程序,那么我们就可以深切体会到Scala在角色的实现以及与角色交互方面的简洁和优雅。下面让我们来看看Scala版本的Primes类是如何实现的:


01 class Primes extends Actor {
02 def receive = {
03 case (lower : Int, upper : Int) =>
04 val count = PrimeFinder.countPrimesInRange(lower, upper)
05 self.replySafe(new Integer(count))
06 }
07 }
08 object Primes {
09 def countPrimes(number : Int, numberOfParts : Int) = {
10 val chunksPerPartition : Int = number / numberOfParts
11 val results = new Array[Future[Integer]](numberOfParts)
12 var index = 0
13 while(index < numberOfParts) {
14 val lower = index * chunksPerPartition + 1
15 val upper = if (index == numberOfParts - 1)
16 number else lower + chunksPerPartition - 1
17 val bounds = (lower, upper)
18 val primeFinder = Actor.actorOf[Primes].start()
19 results(index) = (primeFinder !!! bounds).asInstanceOf[Future[Integer]]
20 index += 1
21 }
22 var count = 0
23 index = 0
24 while(index < numberOfParts) {
25 count += results(index).await.result.get.intValue()
26 index += 1
27 }
28 Actors.registry.shutdownAll
29 count
30 }
31 def main(args : Array[String]) : Unit = {
32 if (args.length < 2)
33 println("Usage: number numberOfParts")
34 else {
35 val start = System.nanoTime
36 val count = countPrimes(args(0).toInt, args(1).toInt)
37 val end = System.nanoTime
38 println("Number of primes is " + count)
39 println("Time taken " + (end - start)/1.0e9)
40 }
41 }
42 }

Scala版本的代码与Java版本有几点不同。首先,Scala版本所使用的消息格式是简单的元组而不是一个不可变列表。其次,receive()函数中的case语句与应用场景十分契合。第三,Java版本中countPrimes()函数里的for循环在这里变成了一个while循环。其原因是,虽然Scala的for循环表达式十分优雅,但会增加Object到基本类型之间的转换开销。为了能够得到比较真实的性能对比,我在这里放弃了优雅。

类似地,在PrimeFinder中,我们也用while循环代替了for循环。


01 object PrimeFinder {
02 def isPrime(number : Int) : Boolean = {
03 if (number <= 1) return false
04 var limit = scala.math.sqrt(number).toInt
05 var i = 2
06 while(i <= limit) {
07 if(number % i == 0) return false
08 i += 1
09 }
10 return true
11 }
12 def countPrimesInRange(lower : Int, upper : Int) : Int = {
13 var count = 0
14 var index = lower
15 while(index <= upper) {
16 if(isPrime(index)) count += 1
17 index += 1
18 }
19 count
20 }
21 }

令待统计区间为[1,1000w]、划分的子区间为100个,则Scala版示例程序的性能如下所示:


1 Number of primes is 664579
2 Time taken 3.88375
目录
相关文章
|
19天前
|
设计模式 API 数据库
【C/C++ 设计思路】C++中解耦策略的艺术:有效管理复杂依赖关系
【C/C++ 设计思路】C++中解耦策略的艺术:有效管理复杂依赖关系
51 3
|
4月前
|
SQL 数据安全/隐私保护
怎样解决上下级关系文件查看的权限控制问题
怎样解决上下级关系文件查看的权限控制问题
25 0
|
Java 数据采集 数据中心
分布式网站隔离术
隔离是将系统或资源分隔开,系统隔离是为了在系统发生故障时,能限定传播范围和影响范围,即发生故障后不会出现滚雪球效应,从而保证只有出问题的服务不可用,其他的服务还是可用的。
1018 0
|
存储 固态存储 数据中心
云概念正在被滥用
本文讲的是云概念正在被滥用,我之前想在这篇博文中讨论SSD(固态驱动器)和磁盘阵列控制器架构的当前情况--一些厂商可能已经准备好让他们现有的阵列处理更多的SSD。
1368 0
|
Java 测试技术 Scala
讨喜的隔离可变性(八)类型化角色和Murmurs
声明:本文是《Java虚拟机并发编程》的第五章,感谢华章出版社授权并发编程网站发布此文,禁止以任何形式转载此文。 使用了类型化角色的EnergySource使我们能够以调用函数的形式来掩盖后台顺序处理异步消息的过程,在实现了线程安全的同时又可以免去显式同步的困扰。
1906 0
|
Java Scala
讨喜的隔离可变性(十)使用Transactor
声明:本文是《Java虚拟机并发编程》的第五章,感谢华章出版社授权并发编程网站发布此文,禁止以任何形式转载此文。 Akka transactor或事务角色为我们提供了一种将多个角色的执行过程合并到一个事务中的方法。
1651 0