中国领先的IT技术网站
|
|

Inside Scala:王在祥的Scala学习笔记

本文是王在祥先生的Scala学习博文系列。在这个系列中,王在祥先生分篇讲述了Scala中的一些有意思的语言特性,如curry,case类,模式匹配等等。

作者:王在祥来源:Wang Zai Xiang|2009-11-16 17:04

【沙龙】51CTO诚邀您9月23号和多位技术大咖一起聊智能CDN的优化之路,抓紧时间哦!


编者注:本系列来自王在祥先生的博客,主要是分篇总结了对于Scala一些语言特性的心得,为了便于与大家分享而在此转载。这个系列适合对Scala以及函数式语言有一定了解的开发者阅读。

51CTO编辑推荐:Scala编程语言专题

Inside Scala - 1:Partially applied functions

Partially applied function(不完全应用的函数)是scala中的一种curry机制,本文将通过一个简单的实例来描述在scala中 partially applied function的内部机制。

  1. // Test3.scala  
  2. package test  
  3.  
  4. object Test3 {  
  5.  
  6.   def sum(x:Int, y:Int, z:Int) = x + y + z  
  7.     
  8.   def main(args: Array[String]) {  
  9.     val sum1 = sum _  
  10.     val sum2 = sum(1, _:Int, 3)  
  11.     println(sum1(1,2,3))  
  12.     println(sum2(2))  
  13.     List(1,2,3,4).foreach(println);  
  14.     List(1,2,3,4).foreach(println _)  
  15.   }  
  16.  
  17. }  
  18.  

在这个代码中 sum _ 表示了一个 新的类型为 (Int,Int,Int)=>Int 的函数,实际上,Scala 会生成一个新的匿名函数(是一个函数对象,Function3),这个函数对象的apply方法会调用 sum 这个对象方法(在这里,是方法,而不是一个函数)。
sum2 是一个 Int => Int的函数(对象),这个函数的apply方法会调用 sum 对象方法。
后面的两行代码都需要访问 println, println是在在Predef对象中定义的方法,在scala中,实际上都会生成一个临时的函数对象,来包装对 println 方法的调用。如果研究一下scala生成的代码,那么可以发现,目前生成的代码中, 对 println, println _生成的代码是重复的,这也说明,目前,所有的你匿名函数基本上没有进行重复性检查。(这可能导致编译生成的的类更大)。

从这里可以得知,虽然,在语法层面,方法(所有的def出来的东西)与函数看起来是一致的,但实际上,二者在底层有区别,方法仍然是不可以直接定位、传值的,他不是一个对象。而仅仅是JVM底层可访问的一个实体。而函数则是虚拟机层面的一个对象。任何从方法到函数的转换,Scala会自动生成一个匿名的函数对象,来进行相应的转换。

所以, List(1,2,3,4).foreach(println) 在底层执行时,并不是获得了一个println的引用(实际上,根本不存在println这个可访问的对象),而是scala自动产生一个匿名的函数,这个函数会调用println。

当然,将一个函数传递时,Scala是不会再做不必要的包装的,而是直接传递这个函数对象了。

Inside Scala - 2: Curry Functions

Curry,在函数式语言中是很常见的,在scala中,对其有特别的支持。

package test

  1. object TestCurry {  
  2.  
  3.   def sum(x:Int)(y:Int)(z:Int) = x + y + z  
  4.  
  5.   def main(args: Array[String]){  
  6.  
  7.     val sum1: (Int => Int => Int) = sum(1)  
  8.     val sum12: Int => Int = sum(1)(2)  
  9.     val sum123 = sum(1)(2)(3)  
  10.     println(sum1(2)(3))  
  11.     println(sum12(3))  
  12.     println(sum123)  
  13.  
  14.   }  
  15.  
  16. }  
  17.  

在这个例子中, sum 被设计成为一个curried函数,(多级函数?),研究一个函数的实现是很有意思的:

如果看生成的 sum 函数代码,那么,它与 如下编写的
def sum(x:Int, y:Int: z:Int) = x + y + z 是一致的。
而且,如果,你调用sum(1)(2)(3),实际上,scala也并不会产生3次函数调用,而是一次 sum(1,2,3)

也就是说,如果你没有进行 sum(1), sum(1)(2)等调用,那么实际上,上述的代码中根本不会生成额外的函数处理代码。但是,如果我们需要进行一些常用的curry操作时,scala为我们提供了额外的语法级的便利。

Inside Scala - 3: How Trait works

Scala中Trait应该是一个非常强大,但又有些复杂的概念,至少与我,我对trait总是有一些不太明了的地方,求人不如求己,对这些疑问还是自己动手探真的比较好。

还是从一个简单的实例着手。

package test

  1. import java.awt.Point  
  2.  
  3. object TestTrait {  
  4.  
  5.   trait Rectangular {  
  6.     def topLeft: Point  
  7.     def bottomRight: Point  
  8.       
  9.     def left = topLeft.x  
  10.     def top = topLeft.y  
  11.     def right = bottomRight.x  
  12.     def bottom = bottomRight.y  
  13.     def width = right - left  
  14.     def height = bottom - top  
  15.   }  
  16.     
  17.   class Rectangle(val topLeft: Point, val bottomRight: Point) extends Rectangular {  
  18.     override def toString = "I am a rectangle" 
  19.   }  
  20.  
  21. }  
  22.  

对这段代码,我想问如下的几个问题:
Rectangle是如何继承 Rectangular的行为,如 left, right, width, height的?
Rectangular 对应于Java的接口,那么,相关的实现代码又是如何保存的?
其实,这两个问题是相关的。研究这个问题的最直接的办法莫过于直接分析scalac编译后的结果。
这个类编译后包括:
TestTrait.class 这个类
TestTrait$.class 其实就是 object TestTrait这个对象的类。一个object实际上从属于一个类,scala是对其加后缀$
在这个例子中,TestTrait这个对象实际上并未定义新的属性和方法,因此,并没有包含什么内容
TestTrait$Rectangular.class
对应于代码中的Rectangular这个trait,这实际上是一个接口类。对应的就是这个trait中定义的全部方法。包括topLeft, bottomRight以及后续的实现方法left, width等的接口定义

  1. public interface test.TestTrait$Rectangular extends scala.ScalaObject{  
  2.  
  3. public abstract int height();  
  4. public abstract int width();  
  5. public abstract int bottom();  
  6. public abstract int right();  
  7. public abstract int top();  
  8. public abstract int left();  
  9. public abstract java.awt.Point bottomRight();  
  10. public abstract java.awt.Point topLeft();  
  11.  
  12. }  

TestTrait$Rectangular$class.class
这个类实际上是trait逻辑的实现类。由于JVM中,接口是不支持任何的实现代码的,因此,scala将相关的逻辑代码编译在这个类中

  1. public abstract class test.TestTrait$Rectangular$class extends java.lang.Object{  
  2.  
  3. public static void $init$(test.TestTrait$Rectangular); // 在这个例子中,没有trait的初始化相关操作  
  4.   Code:  
  5.    0:   return 
  6.  
  7. public static int height(test.TestTrait$Rectangular);   // 对应于height = bottom - top这个操作的实现  
  8.   Code:  
  9.    0:   aload_0  
  10.    1:   invokeinterface #17,  1//InterfaceMethod test/TestTrait$Rectangular.bottom:()I  
  11.    6:   aload_0  
  12.    7:   invokeinterface #20,  1//InterfaceMethod test/TestTrait$Rectangular.top:()I  
  13.    12:  isub  
  14.    13:  ireturn  
  15.  

更多的方法并不在此罗列。
首先,这个实现类是抽象的,它不需要被实例化。
所有的trait方法,其实接收一个额外的参数,即 this 对象。对对象的任何的访问,如bottom等操作,实际上是直接调用对象的相应操作。
所有的trait方法,都是static的。
TestTrait$Rectangle.class
这个就是Rectangle这个类的代码了。

  1. // 首先,实现类以implements的方式继承了trait所定义的接口。  
  2. public class test.TestTrait$Rectangle extends java.lang.Object implements test.TestTrait$Rectangular,scala.ScalaObject{  
  3.  
  4. // 类的val属性直接对应于一个同名的private字段和相应的读取方法。  
  5. private final java.awt.Point bottomRight;  
  6.  
  7. private final java.awt.Point topLeft;  
  8.  
  9. // scala对象比较特殊的是,相应字段的初始化比调用父类构造函数来得更早。也就是说,在Class(arg)中的参数是最早被初始化的。  
  10. // 在构造函数后,可以看到,会调用trait的初始化代码。当然,在我们的这个例子中,trait没有任何的初始化行为。  
  11. public test.TestTrait$Rectangle(java.awt.Point, java.awt.Point);  
  12.   Code:  
  13.    0:   aload_0  
  14.    1:   aload_1  
  15.    2:   putfield        #13//Field topLeft:Ljava/awt/Point;  
  16.    5:   aload_0  
  17.    6:   aload_2  
  18.    7:   putfield        #15//Field bottomRight:Ljava/awt/Point;  
  19.    10:  aload_0  
  20.    11:  invokespecial   #20//Method java/lang/Object."":()V  
  21.    14:  aload_0  
  22.    15:  invokestatic    #26//Method test/TestTrait$Rectangular$class.$init$:(Ltest/TestTrait$Rectangular;)V  
  23.    18:  return 
  24.  
  25. // height这个函数是从trait中继承的,在这里,继承体现为对trait实现类的一个调用,同时,将对象本身作为this传递给该函数  
  26. public int height();  
  27.   Code:  
  28.    0:   aload_0  
  29.    1:   invokestatic    #39//Method test/TestTrait$Rectangular$class.height:(Ltest/TestTrait$Rectangular;)I  
  30.    4:   ireturn  
  31.  

这里不再罗列其他的函数实现,其基本与height函数是相一致的。

理解了以上的逻辑,trait是如何实现将接口和接口实现溶于一体的,应该就非常的清楚了。我以前一直在纳闷一个问题:接口中不能够包含实现代码,那么,难道每次编译继承trait的类时,这写实现的代码是怎么在子类中继承的呢?难道是编译器将这个逻辑复制了一份?如果这样,不仅生成的代码量很大,而且,还有一个问题,那就是,在编译时需要有trait的源代码才行。经过上面的剖析,我们终于知道scala其实有更完美的解决之道的:那就是一个trait辅助类。

Inside Scala - 4: Trait Stacks

这个例子摘自 Programming In Scala 这本书第12.5节。本文将从另外一个角度来分析 Stackable Trait的内部原理。

package test

  1. import scala.collection.mutable.ArrayBuffer  
  2.  
  3. object Test7 {  
  4.  
  5.   abstract class IntQueue {  
  6.     def put(x:Int)  
  7.     def get(): Int  
  8.   }  
  9.     
  10.   class BasicIntQueue extends IntQueue {  
  11.     private val buf = new ArrayBuffer[Int]  
  12.     def put(x:Int) { buf += x }  
  13.     def get() = buf.remove(0)  
  14.   }  
  15.  
  16.   trait Doubling extends IntQueue {  
  17.     abstract override def put(x:Int) { super.put(2*x) }  
  18.   }  
  19.  
  20.   def main(args: Array[String]) {  
  21.     val queue: IntQueue = new BasicIntQueue with Doubling  
  22.     queue.put(1)  
  23.     queue.put(5)  
  24.     println( queue.get )  
  25.     println( queue.get )  
  26.   }  
  27.  
  28. }  
  29.  

我们来看这一行代码 val queue = new BasicIntQue with Doubling,Scala针对这一行代码干了很多很多的工作,并不是一个简单的操作那么简单
Scala需要新生成一个类型,在我的环境中,这个类叫做:Test7$$anon$1,看看这个代码:
// 新的类以BasicIntQueue为父类,同时实现了Doubling这个trait定义的接口

  1. public final class test.Test7$$anon$1 extends test.Test7$BasicIntQueue implements test.Test7$Doubling{  
  2.  
  3. public test.Test7$$anon$1();  
  4.   Code:  
  5.    0:   aload_0  
  6.    1:   invokespecial   #10//Method test/Test7$BasicIntQueue."":()V // 父类初始化  
  7.    4:   aload_0  
  8.    5:   invokestatic    #16//Method test/Test7$Doubling$class.$init$:(Ltest/Test7$Doubling;)V // trait辅助类初始化  
  9.    8:   return 
  10.  
  11. public void put(int);  
  12.   Code:  
  13.    0:   aload_0  
  14.    1:   iload_1  
  15.    2:   invokestatic    #21//Method test/Test7$Doubling$class.put:(Ltest/Test7$Doubling;I)V // 这个类使用的是Doubling提供的版本  
  16.    5:   return 
  17.  
  18. public final void test$Test7$Doubling$$super$put(int); // Doubling所需要的super的版本  
  19.   Code:  
  20.    0:   aload_0  
  21.    1:   iload_1  
  22.    2:   invokespecial   #29//Method test/Test7$BasicIntQueue.put:(I)V  
  23.    5:   return 
  24.  
  25. }  
  26.  

我们来分析一下Doubling这个trait的实现

  1. public interface test.Test7$Doubling extends scala.ScalaObject{  
  2.  
  3. public abstract void put(int);  // 这个是trait中实现的方法  
  4.  
  5. public abstract void test$Test7$Doubling$$super$put(int); // 这个是这个trait 额外依赖的方法  
  6.  
  7. }  
  8.  
  9. // Doubling这个trait的辅助类  
  10. public abstract class test.Test7$Doubling$class extends java.lang.Object{  
  11. public static void $init$(test.Test7$Doubling);  
  12.   Code:  
  13.    0:   return 
  14.  
  15. public static void put(test.Test7$Doubling, int);  
  16.   Code:  
  17.    0:   aload_0  
  18.    1:   iconst_2  
  19.    2:   iload_1  
  20.    3:   imul  
  21.    4:   invokeinterface #17,  2//InterfaceMethod test/Test7$Doubling.test$Test7$Doubling$$super$put:(I)V  
  22. // 这也是 Doubling这个接口中需要 super.init这个方法的原因。  
  23.    9:   return 
  24.  
  25. }  
  26.  

由此可见,编译器在处理 val queue: IntQueue = new BasicIntQueue with Doubling这一行代码时,需要确定类、Trait的先后顺序。这也是理解Trait的最为复杂的一环。后续,我将就这个问题进行分析。

Inside Scala - 5: Trait Stacks

继续上一个案例,现在我们将Trait的链搞得更长一些:

  1. trait Incrementing extends IntQueue {  
  2. abstract override def put(x: Int) { super.put(x + 1) }  
  3. }  
  4. trait Filtering extends IntQueue {  
  5. abstract override def put(x: Int) {  
  6. if (x >= 0super.put(x)  
  7. }  
  8. }  
  9.  
  10. val queue: IntQueue = new BasicIntQueue with Incrementing with Filtering  
  11.  

新的类如何呢?当我们调用 queue的 put方法时,这个的先后顺序究竟如何呢?还是看看生成的代码:

  1. public final class test.Test7$$anon$1 extends test.Test7$BasicIntQueue implements test.Test7$Incrementing,test.Test7$Filtering{  
  2.  
  3. // 初始化的顺序:先父类、再Incremeting、再Filtering,这个顺序与源代码的顺序是一致的。  
  4. public test.Test7$$anon$1();  
  5.   Code:  
  6.    0:   aload_0  
  7.    1:   invokespecial   #10//Method test/Test7$BasicIntQueue."":()V  
  8.    4:   aload_0  
  9.    5:   invokestatic    #16//Method test/Test7$Incrementing$class.$init$:(Ltest/Test7$Incrementing;)V  
  10.    8:   aload_0  
  11.    9:   invokestatic    #21//Method test/Test7$Filtering$class.$init$:(Ltest/Test7$Filtering;)V  
  12.    12:  return 
  13.  
  14. // put 方法实际使用的是 Filtering这个Trait的put  
  15. public void put(int);  
  16.   Code:  
  17.    0:   aload_0  
  18.    1:   iload_1  
  19.    2:   invokestatic    #34//Method test/Test7$Filtering$class.put:(Ltest/Test7$Filtering;I)V  
  20.    5:   return 
  21.  
  22. // Filtering Trait的父实现是Incremeting trait  
  23. public final void test$Test7$Filtering$$super$put(int);  
  24.   Code:  
  25.    0:   aload_0  
  26.    1:   iload_1  
  27.    2:   invokestatic    #38//Method test/Test7$Incrementing$class.put:(Ltest/Test7$Incrementing;I)V  
  28.    5:   return 
  29.  
  30. // incrementing的父实现是父类的实现。  
  31. public final void test$Test7$Incrementing$$super$put(int);  
  32.   Code:  
  33.    0:   aload_0  
  34.    1:   iload_1  
  35.    2:   invokespecial   #26//Method test/Test7$BasicIntQueue.put:(I)V  
  36.    5:   return 
  37.  
  38. }  
  39.  

因此,要理解这个过程,可以这么来分析:val queue: IntQueue = new BasicIntQueue with Incrementing with Filtering
首先初始化的是BasicIntQueue
在这个基础上叠加 Incrementing,super.put引用的是BasicIntQueue的put方法
再在叠加后的基础上叠加 Filtering,super.put引用的是 Incrementing的put方法
叠加后的结果就是最后的版本。put引用的是Filtering的put方法
因此,初始化的顺序是从左至右,而方法的可见性则是从右至左(可以理解为上面的叠加关系,叠加之后,上面的trait具有更大的优先可见性。

Inside Scala - 6:Case Class 与 模式匹配

本文将尝试对Case Class是如何参与模式匹配的进行剖析。文中的代码还是来自 Programming In Scala一书。

  1. abstract class Expr;  
  2. case class Var(name: String) extends Expr;  
  3. case class Number(num: Double) extends Expr;  
  4. case class UnOp(operator: String, arg: Expr) extends Expr;  
  5. case class BinOp(operator:String, left: Expr, right: Expr) extends Expr; 

这里我们先来看一个最为简单的模式匹配

  1. some match {  
  2.   case Var(name) => println("a var with name:" + name)  


这几行的代码编译后等效于:

  1. if(some instanceof Var)  
  2. {  
  3.     Var temp21 = (Var)some;  
  4.     String name = temp21.name();  
  5.     if(true)  
  6.     {  
  7.         name = temp22;  
  8.         Predef$.MODULE$.println((new StringBuilder()).append("a var with name:").append(name).toString());  
  9.     } else 
  10.     {  
  11.         throw new MatchError(some.toString());  
  12.     }  
  13. else 
  14. {  
  15.     throw new MatchError(some.toString());  


如果从生成的代码的角度上来看,Scala生成的代码质量并不高,其中的 if(true) else 的那个部分就有明显的废代码。(不过,这个对运行效率的影响到时几乎可以忽略,只是编译后的字节码倒是没理由的多了几分)。
上面的这个模式匹配仅仅是匹配一个类型。因此,其对应的java原语就是 instanceof 检测。

让我们更进一步, 看看如下的例子:

  1. some match {  
  2.   case Var("x") => println("a var with name:x")  

这个模式匹配不仅匹配类型,还要匹配构造器中的name属性为 "x"常量。这里我就不在福州 Scala生成的字节码了,而是简单的翻译一下:
if( some instanceof Var)  -- 类型检查
var.name() == "x"             -- 检查 对象的 name 属性是否等于 "x",编译器非常清楚的指导 Case Class的每一个构造参数所对应的字段名称。

更进一步,让我们看看一个更复杂的模式匹配:嵌套的对象。

  1. some match {  
  2.   case BinOp("+", Var("x"), UnOp("-", Number(num))) => println("x - " + num)  

这个逻辑其实也是上面的一个嵌套:
some instanceof BinOp
some.operator == "+"  编译器进行了特殊的null检测,以防止这个操作出现NPE
some.left instanceof Var
some.left.name == "x"
some.right instanceof UnOp
some.right.operator == "-"
some.right.arg instanceof Number
......
实际上,Scala的模式匹配确实为我们干了很多很多的事情,这也使得在很多的情况下,使用scala的模式匹配为我们提供了一个非常安全的(不用担心大量的Null检查),以及非常复杂的匹配操作。当然,与更复杂的模式匹配相比(譬如,规则引擎其实也是一个模式匹配的引擎),Scala的模式匹配还是相对比较简单的。

这里简单的补充一下 Scala中的几种模式:
1、通配符模式。 也就是说使用 case _ => 来匹配所有的东西。或者,case Var(_) 来对局部进行通配。
2、常量匹配。譬如上述的Var("x") ,其中,"x"就是一个常量。常量除了文字常量外,还可以使用以大写字母开头的scala变量,或者`varname`形式的引用。
3、变量匹配。一个变量匹配实际上匹配任何的类型,并同时赋予其一个变量名。
4、构造函数匹配。匹配一个给定的类型,并且嵌套的对其参数进行匹配。参数可以是通配符模式、常量、变量或者子构造函数匹配
5、对于List类型, _*可以匹配剩余的全部元素。
6、Tuple匹配。(a,b,c)
7、类型匹配。对于java对象,由于并不适合Scala的Case Class模型,因此,可以使用类型进行匹配。在这种情况下,与构造子匹配是不同的。

再摘一段我以前编写的使用scala来编写应用程序的逻辑代码,让我们看看模式匹配在商业应用中的使用:

  1. _req.transType match {  
  2.       case RechargeEcp | RechargeGnete | FreezeToAvailable => // 充值类交易  
  3.         assert(_req.amount > 0"金额不正确")  
  4.       case DirectPay | AvailableToFreeze =>    // 支付、冻结类交易  
  5.         assert(_req.amount < 0"金额不正确")  
  6.       case _ =>      
  7.         assert(false"无效交易类型")  
  8.     }  
  9.       
  10.     val _account = queryEwAccount(_req.userId)  
  11.     assert(_account != null"用户尚未开通电子钱包")  
  12.       
  13.     var _accAvail, _accFreeze: EWSubAccount = null 
  14.     var _total: BigDecimal = _req.amount  
  15.     _account.subAccounts.find(_.subTypeCode==Available) match {  
  16.       case Some(x) =>  _accAvail = x;    _total += x.balance  
  17.       case None =>  
  18.     }  
  19.     _account.subAccounts.find(_.subTypeCode==Freeze) match {  
  20.       case Some(x) =>  _accFreeze = x;    _total += x.balance  
  21.       case None=>  
  22.     }  

这个仅仅是一个很简单的应用,试想使用Java的if/else或者switch来进行相同的代码,你不妨看看代码量会增加多少?可读性又会如何呢?

Scala Actor是一种借鉴于Erlang的进程消息机制的并发编程模式,由于Java中不存在Erlang的进程的概念,因此,Scala的Actor在隔离性上是不如Erlang的,譬如,在Erlang中,可以有效的终止一个进程,不仅仅无需担心死锁(根本没有锁),也可以马上释放掉改进程的内存,这种隔离性在某种程度上是更接近于操作系统的进程的。在Java的世界里暂时没有等效的替代品。

(题外话,最近在我们的Open Service Platform中集成了一个类似于操作系统定时调度的机制,可以定时执行一些任务,但是最后,我们仍然决定将部分非交易相关的定时任务,主要是一些日志分析类、管理性批量处理等定时任务放到操作系统上进行调度,毕竟操作系统提供了一个更好的虚拟机,在OSGi层面仍然是有限的隔离,哪一天JVM能够提供像操作系统的隔离特性,那么,操作系统就真的不重要了)。

本文将对actor的机制进行简单的分析,以帮助加强对actor的理解。

  1. package learn.actor  
  2.  
  3. object Test1 extends Application {  
  4.  
  5.   import scala.actors.Actor._  
  6.  
  7.   val actor1 = actor {  
  8.  
  9.     println("i am in " + Thread.currentThread)  
  10.  
  11.     while(true) {  
  12.  
  13.       receive {  
  14.  
  15.         case msg => println("recieve msg:" + msg + " In " + Thread.currentThread);  
  16.  
  17.       }  
  18.  
  19.     }  
  20.  
  21.   }  
  22.  
  23.    
  24.  
  25.   val actor2 = actor {  
  26.  
  27.     println("i am in " + Thread.currentThread)  
  28.  
  29.     while(true) {  
  30.  
  31.       receive {  
  32.  
  33.         case msg: String => println("recieve msg:" + msg.toUpperCase + " In " + Thread.currentThread);  
  34.  
  35.       }  
  36.  
  37.     }  
  38.  
  39.   }  
  40.  
  41.    
  42.  
  43.   actor1 ! "Hello World" 
  44.  
  45.   actor2 ! "Hello World" 
  46.  
  47.   actor1 ! "ok" 
  48.  
  49.   actor2 ! "ok" 
  50.  
  51.    
  52.  
  53. }  
  54.  

运行的结果是:

  1. i am in Thread[pool-1-thread-1,5,main]  
  2. i am in Thread[pool-1-thread-2,5,main]  
  3. recieve msg:HELLO WORLD In Thread[pool-1-thread-2,5,main]  
  4. recieve msg:Hello World In Thread[pool-1-thread-1,5,main]  
  5. recieve msg:OK In Thread[pool-1-thread-2,5,main]  
  6. recieve msg:ok In Thread[pool-1-thread-1,5,main] 

从这个例子来看,actor1和actor2实际上是两个独立的Java线程,任何线程可以将消息以 ! 的方式发给给这个线程进行处理。由于采用消息的方式来进行通信,因此,线程与线程之间无需采用Java的notify/wait机制,而后者是建立在锁的基础之上的。有关于这一点,我不在本文只进行深入的分析了。(有必要的话,我会再写一个帖子来说明)。

那么 Scala Actor 的底层基础是什么呢?与Java的notify/wait就完全没有关系吗?我们将重点分析actor的三个方法:!, receive, react

1、Scala Actor的send(外部调用者发送一个消息给当前actor)和receive(当前actor接收一个消息),这两个操作是同步的(synchronized),也就是说,不可同时进入。(客观的说,这一块应该有很大的优化空间,应该采用乐观锁的机制,可能会有更好的效率,一来,send/receive操作本身都是很快速的操作,即便在出现冲突的情况下,使用乐观锁也可以降低线程切换引起的开销,而且,在大部分情况下,send操作与receive操作引发冲突的可能性并不是很大的。也就是说,在很大的程度上,send和receive还可以有更好的并行性,不知道后续的scala版本是否会进行优化。)

2、执行send操作时,如果当前actor正在等待这个消息(指actor自身已经在receive、react并且期待这个消息的情况下),那么原来的等待将会马上执行,否则,消息会进入到actor的邮箱,等待下次receive/react的处理。这种模式相较于全部放入邮箱更加有效。它避免了一次在邮箱上的同步等待。

3、当执行receive操作时,actor会检查对象的邮箱,如果有匹配的消息的话,则会马上返回该消息进行处理,否则会处在等待状态(当前线程阻塞,采用的是wait原语)当匹配的消息到达时,也是采用notify原语通知等待线程继续actor的处理的。

4、react与receive不同的是,react从不返回。这个在Java的编程世界里,好像还没有看到类似的东西,该如何理解它呢:

react(f: ParticialFunction[Any,Unit]) 首先检查actor的邮箱,如果有符合f的消息,则马上提取该消息,并且在一个ExecutionPool中调度执行f。(因此,f的执行肯定不在请求react这个线程中执行的。当前的调用react的线程,将产生一个 SuspendActorException,从而中断一般的执行过程。(也就是说文档中说的不返回的概念)

如果当前邮箱中没有消息,react将登记一个Continuation对象,将等待的消息(一个等待给定消息的函数)、获得消息后需要继续进行的处理在actor中进行登记,而后,当前线程会产生一个SuspendActorException,中断处理(从而是将当前线程归还到线程池)。

当消息到达(通过send)时,send将检查等待消息的Continuation,如过匹配的话,则会在线程池中的选择一个线程来执行f函数。在f处理完成一个消息后,一般的,它会再次调用 react来处理下一个消息,将再次重复这个过程。

应该说,scala的这个设计是非常精巧,也非常有效的,但这对Java开发程序员来说,就意味着一个新的挑战:看上去的一个函数体,实际上其中的代码不仅是执行不连续的(如closure可能会延迟、重复多次的被调用),甚至可能是在不同的线程中被执行的。

从这个概念上来看,scala的actor并不对应于Java的线程,相反,可以理解为一个行为执行者,是一个有上下文的非操作系统线程,语义其实更接近于现实的一个载体。这个与Erlang的进程还是有很明显的语义上的区别的。从上述的分析中,或许如果切换到乐观锁的机制,Scala的并发效率还能有更进一步的提升。

【责任编辑:杨赛 TEL:(010)68476606】


点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

C语言核心技术

在这本书中,C 语言专家 Peter Prinz和Tony Crawford为你提供大量的编程参考信息。全书叙述清晰,语句简洁,分析深刻。本书主题包括: ...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊
× 学习达标赢Beats耳机