1. 程式人生 > >Scala學習筆記(四)react/receive對比

Scala學習筆記(四)react/receive對比

這回繼續研究Actor的應用,我發現scala-lang裡關於Actor的Advance Example很有代表性,所以專門花時間研究一下這個例子,以下我經過我修正後的程式碼並且加入了一些關鍵的debug資訊,因為原始的版本無法在Scala2.8上執行:

import scala.actors._
import scala.actors.Actor._

object Message {
  def main(args: Array[String]) {
    val n = 3
    var k = 0
    
    val nActors = 500
    val finalSum = n * nActors

    def beh(next: Actor, sum: Int) {
      k = k+1
      react {
        case value: Int =>
          val j = value + 1; val nsum = sum + j
          println(Thread.currentThread().getId()+"nSum:"+nsum+", value:"+value+", sum:"+sum)
          if (next == null && nsum >= finalSum) {
            println("End");
            System.exit(0)
          }
          else {
            if (next != null) next ! j
            //react never return, so must call some code to continue
            beh(next, nsum)
          }
      	}
    }
    
    def actorChain(i: Int, a: Actor): Actor =
      if (i > 0) actorChain(i-1, actor(beh(a, 0))) else a
    
    val firstActor = actorChain(nActors, null)
    println("#call times:"+k);
    var i = n; while (i > 0) { firstActor ! 0; i -= 1 }
    
    println("Let's go!");
  }
}


這個例子的意圖是實用多執行緒技術計算++操作,直到達到目標值finalSum為止。首先做的是遞迴的建立500個actor物件,使用的語法是actor{body}:Unit的方式,每一個actor都擁有前面建立那個的引用,這樣可以測試兩個actor互相發訊息,就像擊鼓傳花的那種效果。

開始執行的時候,由firstActor開始,注意到這裡初始同時向firstActor傳送了3次訊息,每次都是一個Int: 0,然後在接收訊息的時候,我特意列印了當前執行緒的ID並記錄了各個關鍵運算引數的值。可能令人費解的是react{}的使用,以及else裡beh又遞迴的呼叫了自己。通常來說我們在actor裡都是使用receive或者receiveWithin接收訊息,但是這兩種方式有個致命的弱點,就是每次接收一次訊息都必須使用新的Thread,所以如果我們改用receive或者receiveWithin來接收訊息,那麼你會看到幾百個Thread ID。下面給出使用receive和receiveWithin版本的程式碼片段:(注意receiveWithin必須要加上TIMEOUT的case,否則無法執行)

//receiveWithin
 receiveWithin(1000) {
        case value: Int =>
          val j = value + 1; val nsum = sum + j
          println(Thread.currentThread().getId()+"nSum:"+nsum+", value:"+value+", sum:"+sum)
          if (next == null && nsum >= finalSum) {
            println("End");
            System.exit(0)
          }
          else {
            if (next != null) next ! j
          }
          
        case TIMEOUT =>
        	System.exit(-1);
      	}
}

//receive version
receive {
        case value: Int =>
          val j = value + 1; val nsum = sum + j
          println(Thread.currentThread().getId()+"nSum:"+nsum+", value:"+value+", sum:"+sum)
          if (next == null && nsum >= finalSum) {
            println("End");
            System.exit(0)
          }
          else {
            if (next != null) next ! j
          }
	}
}

所以出於效能考慮,這裡就使用了react,因為它會使用一種更為合理的任務領取方式來申請Thread資源,多次訊息的傳送和接受都是由總的執行緒池來安排最優化的執行緒數量來執行。在筆者的機器上一共就看到4個不同的Thread ID。另外,使用react的一個特點是它每次執行完了case,不會return任何東西,也就是說程式碼不能在繼續往下走。這個時候一個通常的做法都是遞迴的呼叫包含react的自身,這樣就會回到react的起點,然後等待執行緒池安排任何一個Thread來領取任務,完成訊息的處理。

由於使用了react,只用少量的Thread資源就反覆的處理了眾多的訊息最後完成值計算超過finalSum成功End退出。在筆者的機器上call times的值是會變化而且多執行幾次次數還相差不小。這是因為眾多訊息的處理是由隨機的Thread物件來領取的,可能是由next ! j引起的,那麼這個Thread就能獲得當前beh物件的上下文以及sum, nSum的值;同時,也有可能是遞迴呼叫beh(next, nSum)引起的,那麼這個Thread拿到的是另外一套上下文和變數值。總之誰先到finalSum,程式就結束。這個值可以通過筆者打印出來的log仔細分析一下,尤其是同receive和receiveWithin版本對比一下,相信你會有所收穫的。