Scala Actors: Different behavior on JRE 1.5 and 1.6
Asked Answered
S

2

16

My simulation is using actors and Scala 2.8-Snapshot. In Java JRE 1.5 it runs well - all 40 gears (actors) are working simultaneously. Using Java JRE 1.6 only 3 gears are working simultaneously. I tested it with and without GUI: both give same result.

My simulation with GUI is available on github: http://github.com/pmeiclx/scala_gear_simulation

Maybe you remember to my first problem with actors. After solving these problems I did a GUI for the simulation and I got this new "strange" behavior.

Here's the code without GUI:

package ch.clx.actorversions

import actors.Actor
import actors.Actor._
import collection.mutable.ListBuffer

case class ReceivedSpeed(gear: Gear)
case object StartSync

case class SyncGear(controller: GearController, syncSpeed: Int)

object ActorVersion {

  def main(args:Array[String]) = {
    println("[App] start with creating gears")
    val gearList = new ListBuffer[Gear]()
    for (i <- 0 until 100) {
      gearList += new Gear(i)
    }

    val gearController = new GearController(gearList)

    gearController.start()
    gearController ! StartSync
  }
}

/**
 * CONTROLLER
 */
class GearController(nGears: ListBuffer[Gear]) extends Actor {
  private var syncGears = new ListBuffer[Gear]
  private var syncSpeed = 0
  def act = {
    while(true) {
      receive {
        case StartSync => {
          println("[Controller] Send commands for syncing to gears!")
          var speeds = new ListBuffer[Int]
          nGears.foreach(e => speeds += e.speed)

          //Calc avg
          //var avgSpeed = speeds.foldLeft(0)(_ + _) / speeds.length
          //var avgSpeed = speeds.foldLeft(0) { (x, y) => x + y } / speeds.length
          syncSpeed = (0/:speeds)(_ + _) / speeds.length //Average over all gear speeds

          //TODO syncSpeed auf Median ausrichten

          println("[Controller] calculated syncSpeed: "+syncSpeed)
          nGears.foreach{e =>
                         e.start()
                         e ! SyncGear(this, syncSpeed)
          }
          println("[Controller] started all gears")
        }
        case ReceivedSpeed(gear: Gear) => {
          println("[Controller] Syncspeed received by a gear ("+gear.gearId+")")
          //println("[Controller] mailboxsize: "+self.mailboxSize)
          syncGears += gear
          if(syncGears.length == nGears.length) {
            println("[Controller] all gears are back in town!")
            System.exit(0)
          }
        }
        case _ => println("[Controller] No match :(")
      }
    }
  }
}

/**
 * GEAR
 */
class Gear(id: Int) extends Actor {

  private var mySpeed = scala.util.Random.nextInt(1000)
  private var myController: GearController = null

  def speed = mySpeed
  def gearId = id

  /* Constructor */
  println("[Gear ("+id+")] created with speed: "+mySpeed)

  def act = {
    loop {
      react {
        case SyncGear(controller: GearController, syncSpeed: Int) => {
          //println("[Gear ("+id+")] activated, try to follow controller command (form mySpeed ("+mySpeed+") to syncspeed ("+syncSpeed+")")
          myController = controller
          adjustSpeedTo(syncSpeed)
        }
      }
    }
  }

  def adjustSpeedTo(targetSpeed: Int) = {
    if(targetSpeed > mySpeed) {
      mySpeed += 1
      self ! SyncGear(myController, targetSpeed)
    }else if(targetSpeed < mySpeed) {
      mySpeed -= 1
      self ! SyncGear(myController, targetSpeed)
    } else if(targetSpeed == mySpeed) {
      callController
    }
  }

  def callController = {
    println("[Gear ("+id+")] has syncSpeed")
    myController ! ReceivedSpeed(this)
  }
}
Sirotek answered 18/2, 2010 at 13:0 Comment(0)
C
8

Short answer: change your controller to use loop/react instead of while/receive

The actors library detects which Java version it is running on, and if it is 1.6 (and not IBM's VM) it uses a bundled version of the JSR-166y fork join thread pool, so there is a substantial difference in the underlying implementation depending on Java version.

The fork/join thread pool uses a kind of two-level queue for tasks. Each worker thread has a queue, and there's a shared queue for the pool. Tasks originating in a fork/join thread go directly onto the fork/join thread's queue rather than through the main queue. Task stealing among threads is used to keep threads busy and help avoid starvation.

In your case all of the tasks to start the gears end up on queue for the thread running the controller. Because you're using while/receive in that actor it never lets go of the thread, so it never executes the tasks directly on its queue. The other threads are constantly busy with the 3 gears, so they never attempt to steal work from the thread running the controller. The result is the other gear actors are never executed.

Switching to loop/react in the controller should fix the problem because on every loop the actor lets go of the thread and schedules a new task, which will end up at the back of the queue so the other tasks on it will be executed.

Chieftain answered 20/2, 2010 at 15:59 Comment(2)
FYI: I explained this problem to Philipp Haller and he has fixed it in trunk. So when 2.8 is released it should not have the problem. lampsvn.epfl.ch/trac/scala/changeset/20950/scala/trunk/src/…Chieftain
Sorry, i was a little bit busy. With the new Snapshot it works. Not perfect but it works. Thank you!Sirotek
S
1

Using Java JRE 1.6 only 3 gears are working simultaneously.

Do you mean that:

  • only three gears make any progress towards the target speed. When the three gears reach the target speed no more gears make any progress.
  • only three gears make progress at any one time. When one of the three gears reaches the target speed, another gear starts making progress until all gears have reached the target speed.

I would guess the second?

The difference in observed behavior is probably down to a difference in the JVM implementations - there are changes between JRE 1.5 and JRE 1.6. Some of these changes can be switched off, e.g. by setting a flag like this one:

-XX:ThreadPriorityPolicy=1

... but the second behaviour is a totally valid way to execute your code. It just isn't what you expected because it violates a notion of "fairness" that you have but the work scheduler doesn't. You could add some kind of Clock actor to ensure that the most favoured gear receives no more than (say) 10 "ticks" more than the least favoured actor.

The difference between the JREs is hard to research without knowing:

  • exactly which JRE update versions you are using.
  • which OS you run.
  • how many CPUs and cores you have.
  • whether the code has been recompiled for JRE 1.6.

Good luck!

Steinberg answered 20/2, 2010 at 14:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.